-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement meeting protocol system (#123) #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
d1755f6
af5d72a
3101925
2aa2a1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| """Meeting protocol subsystem (DESIGN_SPEC Section 5.7). | ||
|
|
||
| Provides pluggable meeting protocol strategies for structured | ||
| multi-agent conversations: | ||
|
|
||
| - **Round-Robin**: Sequential turns with full transcript context. | ||
| - **Position Papers**: Parallel independent papers, then synthesis. | ||
| - **Structured Phases**: Phased agenda with conditional discussion. | ||
| """ | ||
|
|
||
| from ai_company.communication.meeting.config import ( | ||
| MeetingProtocolConfig, | ||
| PositionPapersConfig, | ||
| RoundRobinConfig, | ||
| StructuredPhasesConfig, | ||
| ) | ||
| from ai_company.communication.meeting.enums import ( | ||
| MeetingPhase, | ||
| MeetingProtocolType, | ||
| MeetingStatus, | ||
| ) | ||
| from ai_company.communication.meeting.errors import ( | ||
| MeetingAgentError, | ||
| MeetingBudgetExhaustedError, | ||
| MeetingError, | ||
| MeetingParticipantError, | ||
| MeetingProtocolNotFoundError, | ||
| ) | ||
| from ai_company.communication.meeting.models import ( | ||
| ActionItem, | ||
| AgentResponse, | ||
| MeetingAgenda, | ||
| MeetingAgendaItem, | ||
| MeetingContribution, | ||
| MeetingMinutes, | ||
| MeetingRecord, | ||
| ) | ||
| from ai_company.communication.meeting.orchestrator import MeetingOrchestrator | ||
| from ai_company.communication.meeting.position_papers import ( | ||
| PositionPapersProtocol, | ||
| ) | ||
| from ai_company.communication.meeting.protocol import ( | ||
| AgentCaller, | ||
| ConflictDetector, | ||
| MeetingProtocol, | ||
| TaskCreator, | ||
| ) | ||
| from ai_company.communication.meeting.round_robin import RoundRobinProtocol | ||
| from ai_company.communication.meeting.structured_phases import ( | ||
| KeywordConflictDetector, | ||
| StructuredPhasesProtocol, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "ActionItem", | ||
| "AgentCaller", | ||
| "AgentResponse", | ||
| "ConflictDetector", | ||
| "KeywordConflictDetector", | ||
| "MeetingAgenda", | ||
| "MeetingAgendaItem", | ||
| "MeetingAgentError", | ||
| "MeetingBudgetExhaustedError", | ||
| "MeetingContribution", | ||
| "MeetingError", | ||
| "MeetingMinutes", | ||
| "MeetingOrchestrator", | ||
| "MeetingParticipantError", | ||
| "MeetingPhase", | ||
| "MeetingProtocol", | ||
| "MeetingProtocolConfig", | ||
| "MeetingProtocolNotFoundError", | ||
| "MeetingProtocolType", | ||
| "MeetingRecord", | ||
| "MeetingStatus", | ||
| "PositionPapersConfig", | ||
| "PositionPapersProtocol", | ||
| "RoundRobinConfig", | ||
| "RoundRobinProtocol", | ||
| "StructuredPhasesConfig", | ||
| "StructuredPhasesProtocol", | ||
| "TaskCreator", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||||
| """Shared prompt builders for meeting protocol implementations.""" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from typing import TYPE_CHECKING | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||
| from ai_company.communication.meeting.models import MeetingAgenda | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def build_agenda_prompt(agenda: MeetingAgenda) -> str: | ||||||||||||||||||||||||
|
Comment on lines
+3
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: With PEP 649 lazy annotations (Python 3.14+), the annotation expression isn’t evaluated at function definition time, so referencing a name that’s only imported under This matches the general PEP 649/749 model: evaluation is deferred, but when the deferred evaluation runs, missing names can still produce runtime errors like Implication: if you want Sources: 🏁 Script executed: # Check if there's a circular import between models.py and _prompts.py
cd src/ai_company/communication/meeting
echo "=== Checking models.py imports ==="
head -20 models.py | grep -E "^(import|from)" || echo "No imports in first 20 lines"
echo -e "\n=== Checking _prompts.py imports ==="
head -20 _prompts.py | grep -E "^(import|from)" || echo "No imports in first 20 lines"
echo -e "\n=== Checking if models.py imports from _prompts.py ==="
grep -n "_prompts" models.py || echo "No imports from _prompts in models.py"Repository: Aureliolo/ai-company Length of output: 477 🏁 Script executed: # Read full _prompts.py to see the complete function implementation
cat -n src/ai_company/communication/meeting/_prompts.pyRepository: Aureliolo/ai-company Length of output: 1097 🏁 Script executed: # Check MeetingAgenda and MeetingAgendaItem definitions in models.py
cat -n src/ai_company/communication/meeting/models.py | head -100Repository: Aureliolo/ai-company Length of output: 3382 🏁 Script executed: # Find usages of build_agenda_prompt
rg "build_agenda_prompt" --type py -B 2 -A 2Repository: Aureliolo/ai-company Length of output: 3462 🏁 Script executed: # Check for test files
find src/ai_company/communication/meeting -name "*test*" -o -name "test_*"Repository: Aureliolo/ai-company Length of output: 46 🏁 Script executed: # Check if there's documentation about what presenter_id should do
grep -rn "presenter_id" src/ai_company/communication/meeting/ --type pyRepository: Aureliolo/ai-company Length of output: 181 🏁 Script executed: # Search for presenter_id usage in meeting module
rg "presenter_id" src/ai_company/communication/meeting/Repository: Aureliolo/ai-company Length of output: 266 🏁 Script executed: # Check if there are any test files for the prompts
find . -path "*/test*" -name "*prompt*" -o -path "*/test*" -name "*meeting*" | grep -E "(test|spec)" | head -20Repository: Aureliolo/ai-company Length of output: 239 🏁 Script executed: # Check the docstring and usage context more broadly
rg "presenter" src/ai_company/communication/meeting/ -B 2 -A 2Repository: Aureliolo/ai-company Length of output: 888 🏁 Script executed: # Check unit tests for meeting module to see what's expected
ls -la tests/unit/communication/meeting/Repository: Aureliolo/ai-company Length of output: 918 🏁 Script executed: # Look for tests specifically for _prompts.py
cat tests/unit/communication/meeting/*prompt* 2>/dev/null || echo "No prompt tests found, checking all tests"
ls tests/unit/communication/meeting/Repository: Aureliolo/ai-company Length of output: 296 🏁 Script executed: # Check integration test for meeting to see how presenter_id is used
cat tests/integration/communication/test_meeting_integration.py | head -100Repository: Aureliolo/ai-company Length of output: 2998 Use a real import for
Safer import pattern-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from ai_company.communication.meeting.models import MeetingAgenda
+from ai_company.communication.meeting.models import MeetingAgenda📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| """Build the initial agenda prompt text. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||
| agenda: The meeting agenda to format. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||
| Formatted agenda text for use in agent prompts. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| parts = [f"Meeting: {agenda.title}"] | ||||||||||||||||||||||||
| if agenda.context: | ||||||||||||||||||||||||
| parts.append(f"Context: {agenda.context}") | ||||||||||||||||||||||||
| if agenda.items: | ||||||||||||||||||||||||
| parts.append("Agenda items:") | ||||||||||||||||||||||||
| for i, item in enumerate(agenda.items, 1): | ||||||||||||||||||||||||
| entry = f" {i}. {item.title}" | ||||||||||||||||||||||||
| if item.description: | ||||||||||||||||||||||||
| entry += f" — {item.description}" | ||||||||||||||||||||||||
| parts.append(entry) | ||||||||||||||||||||||||
|
Comment on lines
+23
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't drop
📝 Suggested formatter fix for i, item in enumerate(agenda.items, 1):
entry = f" {i}. {item.title}"
+ if item.presenter_id:
+ entry += f" (presenter: {item.presenter_id})"
if item.description:
entry += f" — {item.description}"
parts.append(entry)🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| return "\n".join(parts) | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """Shared token budget tracker for meeting protocol implementations. | ||
|
|
||
| Concurrency note: ``TokenTracker`` is safe for use within a single | ||
| ``asyncio`` event loop (cooperative multitasking). ``record()`` runs | ||
| to completion without suspension, so concurrent coroutines sharing a | ||
| tracker will not interleave reads and writes. However, intermediate | ||
| values of ``remaining`` during a parallel ``TaskGroup`` phase reflect | ||
| only the tasks that have completed so far — callers should pre-divide | ||
| budgets before launching parallel work rather than checking | ||
| ``remaining`` inside concurrent tasks. | ||
| """ | ||
|
|
||
| import dataclasses | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class TokenTracker: | ||
| """Mutable token budget tracker scoped to a single meeting execution. | ||
|
|
||
| Attributes: | ||
| budget: Total token budget for the meeting. | ||
| input_tokens: Total prompt tokens consumed so far. | ||
| output_tokens: Total response tokens generated so far. | ||
| """ | ||
|
|
||
| budget: int | ||
| input_tokens: int = 0 | ||
| output_tokens: int = 0 | ||
|
|
||
| def __post_init__(self) -> None: | ||
| """Validate budget is positive.""" | ||
| if self.budget <= 0: | ||
| msg = f"budget must be positive, got {self.budget}" | ||
| raise ValueError(msg) | ||
|
|
||
| @property | ||
| def used(self) -> int: | ||
| """Total tokens consumed so far.""" | ||
| return self.input_tokens + self.output_tokens | ||
|
|
||
| @property | ||
| def remaining(self) -> int: | ||
| """Tokens remaining in the budget.""" | ||
| return max(0, self.budget - self.used) | ||
|
|
||
| @property | ||
| def is_exhausted(self) -> bool: | ||
| """Whether the budget is fully consumed.""" | ||
| return self.remaining == 0 | ||
|
|
||
| def record(self, input_tokens: int, output_tokens: int) -> None: | ||
| """Record token usage from an agent call. | ||
|
|
||
| Args: | ||
| input_tokens: Prompt tokens consumed (must be >= 0). | ||
| output_tokens: Response tokens generated (must be >= 0). | ||
|
|
||
| Raises: | ||
| ValueError: If either token count is negative. | ||
| """ | ||
| if input_tokens < 0 or output_tokens < 0: | ||
| msg = ( | ||
| f"Token counts must be non-negative, got " | ||
| f"input_tokens={input_tokens}, output_tokens={output_tokens}" | ||
| ) | ||
| raise ValueError(msg) | ||
| self.input_tokens += input_tokens | ||
| self.output_tokens += output_tokens | ||
|
Comment on lines
+16
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Prefer copy-on-write state over mutating This helper introduces shared mutable state via a dataclass and As per coding guidelines, "Create new objects instead of mutating existing ones" and "Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (model_copy(update=...)) for runtime state that evolves; never mix static config fields with mutable runtime fields". 🤖 Prompt for AI Agents
Comment on lines
+41
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when a token record would go negative or exceed the budget.
🛡️ Proposed fix+from ai_company.communication.meeting.errors import MeetingBudgetExhaustedError
+
@@
def record(self, input_tokens: int, output_tokens: int) -> None:
"""Record token usage from an agent call."""
+ if input_tokens < 0 or output_tokens < 0:
+ msg = "token counts must be non-negative"
+ raise ValueError(msg)
+ projected_total = self.used + input_tokens + output_tokens
+ if projected_total > self.budget:
+ msg = (
+ "meeting token budget exhausted: "
+ f"projected_total={projected_total}, budget={self.budget}"
+ )
+ raise MeetingBudgetExhaustedError(msg)
self.input_tokens += input_tokens
self.output_tokens += output_tokens🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update
MeetingTypeConfigdocs for the new public field.protocol_configis now part of the public model, but theMeetingTypeConfigdocstring still stops atduration_tokens. Please document the new attribute there so generated docs and IDE help stay accurate. As per coding guidelines, "All public classes and functions must have Google-style docstrings; ruff D rules enforce this".🤖 Prompt for AI Agents