-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement message and communication domain models #74
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Communication domain models for the AI company framework.""" | ||
|
|
||
| from ai_company.communication.channel import Channel | ||
| from ai_company.communication.config import ( | ||
| CircuitBreakerConfig, | ||
| CommunicationConfig, | ||
| HierarchyConfig, | ||
| LoopPreventionConfig, | ||
| MeetingsConfig, | ||
| MeetingTypeConfig, | ||
| MessageBusConfig, | ||
| RateLimitConfig, | ||
| ) | ||
| from ai_company.communication.enums import ( | ||
| AttachmentType, | ||
| ChannelType, | ||
| CommunicationPattern, | ||
| MessageBusBackend, | ||
| MessagePriority, | ||
| MessageType, | ||
| ) | ||
| from ai_company.communication.message import Attachment, Message, MessageMetadata | ||
|
|
||
| __all__ = [ | ||
| "Attachment", | ||
| "AttachmentType", | ||
| "Channel", | ||
| "ChannelType", | ||
| "CircuitBreakerConfig", | ||
| "CommunicationConfig", | ||
| "CommunicationPattern", | ||
| "HierarchyConfig", | ||
| "LoopPreventionConfig", | ||
| "MeetingTypeConfig", | ||
| "MeetingsConfig", | ||
| "Message", | ||
| "MessageBusBackend", | ||
| "MessageBusConfig", | ||
| "MessageMetadata", | ||
| "MessagePriority", | ||
| "MessageType", | ||
| "RateLimitConfig", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """Channel domain model.""" | ||
|
|
||
| from collections import Counter | ||
| from typing import Self | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field, model_validator | ||
|
|
||
| from ai_company.communication.enums import ChannelType | ||
| from ai_company.core.types import ( | ||
| NotBlankStr, # noqa: TC001 -- required at runtime by Pydantic | ||
| ) | ||
|
|
||
|
|
||
| class Channel(BaseModel): | ||
| """A named communication channel that agents can subscribe to. | ||
|
|
||
| Attributes: | ||
| name: Channel name (e.g. ``"#engineering"``). | ||
| type: Channel delivery semantics. | ||
| subscribers: Agent IDs subscribed to this channel. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| name: NotBlankStr = Field(description="Channel name") | ||
| type: ChannelType = Field( | ||
| default=ChannelType.TOPIC, | ||
| description="Channel delivery semantics", | ||
| ) | ||
| subscribers: tuple[str, ...] = Field( | ||
| default=(), | ||
| description="Agent IDs subscribed to this channel", | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_subscribers(self) -> Self: | ||
| """Ensure subscriber entries are non-blank and unique.""" | ||
| for sub in self.subscribers: | ||
| if not sub.strip(): | ||
| msg = "Empty or whitespace-only entry in subscribers" | ||
| raise ValueError(msg) | ||
| if len(self.subscribers) != len(set(self.subscribers)): | ||
| dupes = sorted(s for s, c in Counter(self.subscribers).items() if c > 1) | ||
| msg = f"Duplicate entries in subscribers: {dupes}" | ||
| raise ValueError(msg) | ||
| return self |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| """Communication configuration models (DESIGN_SPEC Sections 5.4, 5.5).""" | ||
|
|
||
| from collections import Counter | ||
| from typing import Literal, Self | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field, model_validator | ||
|
|
||
| from ai_company.communication.enums import ( | ||
| CommunicationPattern, | ||
| MessageBusBackend, | ||
| ) | ||
| from ai_company.core.types import ( | ||
| NotBlankStr, # noqa: TC001 -- required at runtime by Pydantic | ||
| ) | ||
|
|
||
| # Default channels from DESIGN_SPEC Section 5.4. | ||
| _DEFAULT_CHANNELS: tuple[str, ...] = ( | ||
| "#all-hands", | ||
| "#engineering", | ||
| "#product", | ||
| "#design", | ||
| "#incidents", | ||
| "#code-review", | ||
| "#watercooler", | ||
| ) | ||
|
|
||
|
|
||
| class MessageBusConfig(BaseModel): | ||
| """Message bus backend configuration. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.4 ``message_bus``. | ||
|
|
||
| Attributes: | ||
| backend: Transport backend to use. | ||
| channels: Pre-defined channel names. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| backend: MessageBusBackend = Field( | ||
| default=MessageBusBackend.INTERNAL, | ||
| description="Transport backend", | ||
| ) | ||
| channels: tuple[str, ...] = Field( | ||
| default=_DEFAULT_CHANNELS, | ||
| description="Pre-defined channel names", | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_channels(self) -> Self: | ||
| """Ensure channel names are non-blank and unique.""" | ||
| for ch in self.channels: | ||
| if not ch.strip(): | ||
| msg = "Empty or whitespace-only entry in channels" | ||
| raise ValueError(msg) | ||
| if len(self.channels) != len(set(self.channels)): | ||
| dupes = sorted(c for c, n in Counter(self.channels).items() if n > 1) | ||
| msg = f"Duplicate entries in channels: {dupes}" | ||
| raise ValueError(msg) | ||
| return self | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class MeetingTypeConfig(BaseModel): | ||
| """Configuration for a single meeting type. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.4 ``meetings.types[]``. Exactly one of | ||
| ``frequency`` or ``trigger`` must be set. | ||
|
|
||
| Attributes: | ||
| name: Meeting type name (e.g. ``"daily_standup"``). | ||
| frequency: Recurrence schedule (mutually exclusive with trigger). | ||
| trigger: Event trigger (mutually exclusive with frequency). | ||
| participants: Participant role or agent identifiers. | ||
| duration_tokens: Token budget for the meeting. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| name: NotBlankStr = Field(description="Meeting type name") | ||
| frequency: NotBlankStr | None = Field( | ||
| default=None, | ||
| description="Recurrence schedule", | ||
| ) | ||
| trigger: NotBlankStr | None = Field( | ||
| default=None, | ||
| description="Event trigger", | ||
| ) | ||
| participants: tuple[str, ...] = Field( | ||
| default=(), | ||
| description="Participant role or agent identifiers", | ||
| ) | ||
| duration_tokens: int = Field( | ||
| default=2000, | ||
| gt=0, | ||
| description="Token budget for the meeting", | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_frequency_or_trigger(self) -> Self: | ||
| """Exactly one of frequency or trigger must be set.""" | ||
| if self.frequency is not None and self.trigger is not None: | ||
| msg = "Only one of frequency or trigger may be set, not both" | ||
| raise ValueError(msg) | ||
| if self.frequency is None and self.trigger is None: | ||
| msg = "Exactly one of frequency or trigger must be set" | ||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_participants(self) -> Self: | ||
| """Ensure participant entries are non-blank and unique.""" | ||
| for p in self.participants: | ||
| if not p.strip(): | ||
| msg = "Empty or whitespace-only entry in participants" | ||
| raise ValueError(msg) | ||
| if len(self.participants) != len(set(self.participants)): | ||
| dupes = sorted(p for p, c in Counter(self.participants).items() if c > 1) | ||
| msg = f"Duplicate entries in participants: {dupes}" | ||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
|
|
||
| class MeetingsConfig(BaseModel): | ||
| """Meetings subsystem configuration. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.4 ``meetings``. | ||
|
|
||
| Attributes: | ||
| enabled: Whether the meetings subsystem is active. | ||
| types: Configured meeting types (unique by name). | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| enabled: bool = Field(default=True, description="Meetings subsystem active") | ||
| types: tuple[MeetingTypeConfig, ...] = Field( | ||
| default=(), | ||
| description="Configured meeting types", | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_unique_meeting_names(self) -> Self: | ||
| """Ensure meeting type names are unique.""" | ||
| names = [mt.name for mt in self.types] | ||
| if len(names) != len(set(names)): | ||
| dupes = sorted(n for n, c in Counter(names).items() if c > 1) | ||
| msg = f"Duplicate meeting type names: {dupes}" | ||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
|
|
||
| class HierarchyConfig(BaseModel): | ||
| """Hierarchy enforcement configuration. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.4 ``hierarchy``. | ||
|
|
||
| Attributes: | ||
| enforce_chain_of_command: Whether chain-of-command is enforced. | ||
| allow_skip_level: Whether skip-level messaging is allowed. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| enforce_chain_of_command: bool = Field( | ||
| default=True, | ||
| description="Enforce chain-of-command", | ||
| ) | ||
| allow_skip_level: bool = Field( | ||
| default=False, | ||
| description="Allow skip-level messaging", | ||
| ) | ||
|
|
||
|
|
||
| class RateLimitConfig(BaseModel): | ||
| """Per-pair message rate limit configuration. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.5 ``rate_limit``. | ||
|
|
||
| Attributes: | ||
| max_per_pair_per_minute: Maximum messages per agent pair per minute. | ||
| burst_allowance: Extra burst capacity above the rate limit. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| max_per_pair_per_minute: int = Field( | ||
| default=10, | ||
| gt=0, | ||
| description="Max messages per agent pair per minute", | ||
| ) | ||
| burst_allowance: int = Field( | ||
| default=3, | ||
| ge=0, | ||
| description="Extra burst capacity", | ||
| ) | ||
|
|
||
|
|
||
| class CircuitBreakerConfig(BaseModel): | ||
| """Circuit breaker configuration for agent-pair communication. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.5 ``circuit_breaker``. | ||
|
|
||
| Attributes: | ||
| bounce_threshold: Bounce count before the circuit opens. | ||
| cooldown_seconds: Seconds to wait before retrying after trip. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| bounce_threshold: int = Field( | ||
| default=3, | ||
| gt=0, | ||
| description="Bounce count before circuit opens", | ||
| ) | ||
| cooldown_seconds: int = Field( | ||
| default=300, | ||
| gt=0, | ||
| description="Cooldown period in seconds", | ||
| ) | ||
|
|
||
|
|
||
| class LoopPreventionConfig(BaseModel): | ||
| """Loop prevention safeguards. | ||
|
|
||
| Maps to DESIGN_SPEC Section 5.5. ``ancestry_tracking`` is always on | ||
| and cannot be disabled. | ||
|
|
||
| Attributes: | ||
| max_delegation_depth: Hard limit on delegation chain length. | ||
| rate_limit: Per-pair rate limit settings. | ||
| dedup_window_seconds: Deduplication window in seconds. | ||
| circuit_breaker: Circuit breaker settings. | ||
| ancestry_tracking: Must always be ``True``. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| max_delegation_depth: int = Field( | ||
| default=5, | ||
| gt=0, | ||
| description="Hard limit on delegation chain length", | ||
| ) | ||
| rate_limit: RateLimitConfig = Field( | ||
| default_factory=RateLimitConfig, | ||
| description="Per-pair rate limit settings", | ||
| ) | ||
| dedup_window_seconds: int = Field( | ||
| default=60, | ||
| gt=0, | ||
| description="Deduplication window in seconds", | ||
| ) | ||
| circuit_breaker: CircuitBreakerConfig = Field( | ||
| default_factory=CircuitBreakerConfig, | ||
| description="Circuit breaker settings", | ||
| ) | ||
| ancestry_tracking: Literal[True] = Field( | ||
| default=True, | ||
| description="Task ancestry tracking (always on, not configurable)", | ||
| ) | ||
|
|
||
|
|
||
| class CommunicationConfig(BaseModel): | ||
| """Top-level communication configuration. | ||
|
|
||
| Aggregates DESIGN_SPEC Sections 5.4 and 5.5 under a single model. | ||
|
|
||
| Attributes: | ||
| default_pattern: High-level communication pattern. | ||
| message_bus: Message bus configuration. | ||
| meetings: Meetings subsystem configuration. | ||
| hierarchy: Hierarchy enforcement settings. | ||
| loop_prevention: Loop prevention safeguards. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| default_pattern: CommunicationPattern = Field( | ||
| default=CommunicationPattern.HYBRID, | ||
| description="High-level communication pattern", | ||
| ) | ||
| message_bus: MessageBusConfig = Field( | ||
| default_factory=MessageBusConfig, | ||
| description="Message bus configuration", | ||
| ) | ||
| meetings: MeetingsConfig = Field( | ||
| default_factory=MeetingsConfig, | ||
| description="Meetings subsystem configuration", | ||
| ) | ||
| hierarchy: HierarchyConfig = Field( | ||
| default_factory=HierarchyConfig, | ||
| description="Hierarchy enforcement settings", | ||
| ) | ||
| loop_prevention: LoopPreventionConfig = Field( | ||
| default_factory=LoopPreventionConfig, | ||
| description="Loop prevention safeguards", | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
The validation logic here to ensure channel names are non-blank and unique is very similar to the logic in
_validate_subscribersinsrc/ai_company/communication/channel.py. To improve maintainability and adhere to the DRY principle, consider extracting this logic into a reusable utility function that can be called from bothmodel_validators.