march-agent 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- march_agent/__init__.py +52 -0
- march_agent/agent.py +341 -0
- march_agent/agent_state_client.py +149 -0
- march_agent/app.py +416 -0
- march_agent/artifact.py +58 -0
- march_agent/checkpoint_client.py +169 -0
- march_agent/checkpointer.py +16 -0
- march_agent/cli.py +139 -0
- march_agent/conversation.py +103 -0
- march_agent/conversation_client.py +86 -0
- march_agent/conversation_message.py +48 -0
- march_agent/exceptions.py +36 -0
- march_agent/extensions/__init__.py +1 -0
- march_agent/extensions/langgraph.py +526 -0
- march_agent/extensions/pydantic_ai.py +180 -0
- march_agent/gateway_client.py +506 -0
- march_agent/gateway_pb2.py +73 -0
- march_agent/gateway_pb2_grpc.py +101 -0
- march_agent/heartbeat.py +84 -0
- march_agent/memory.py +73 -0
- march_agent/memory_client.py +155 -0
- march_agent/message.py +80 -0
- march_agent/streamer.py +220 -0
- march_agent-0.1.1.dist-info/METADATA +503 -0
- march_agent-0.1.1.dist-info/RECORD +29 -0
- march_agent-0.1.1.dist-info/WHEEL +5 -0
- march_agent-0.1.1.dist-info/entry_points.txt +2 -0
- march_agent-0.1.1.dist-info/licenses/LICENSE +21 -0
- march_agent-0.1.1.dist-info/top_level.txt +1 -0
march_agent/cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CLI interface for March AI Agent framework.
|
|
2
|
+
|
|
3
|
+
Inspired by uvicorn's simple pattern: march-agent run main:app
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import argparse
|
|
8
|
+
import importlib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def import_from_string(import_str: str):
|
|
13
|
+
"""
|
|
14
|
+
Import an attribute from a module using uvicorn-style string.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
"main:app" → imports 'app' from 'main' module
|
|
18
|
+
"main:setup" → imports 'setup' from 'main' module
|
|
19
|
+
"main" → imports 'main' module and looks for 'app' attribute
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
import_str: Import string in format "module:attribute" or "module"
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
tuple: (module, attribute_name, attribute_value)
|
|
26
|
+
"""
|
|
27
|
+
# Add current directory to path
|
|
28
|
+
sys.path.insert(0, str(Path.cwd()))
|
|
29
|
+
|
|
30
|
+
# Parse import string
|
|
31
|
+
if ":" in import_str:
|
|
32
|
+
module_str, attr_str = import_str.split(":", 1)
|
|
33
|
+
else:
|
|
34
|
+
module_str = import_str
|
|
35
|
+
attr_str = "app" # Default to 'app' like uvicorn
|
|
36
|
+
|
|
37
|
+
# Import module
|
|
38
|
+
try:
|
|
39
|
+
module = importlib.import_module(module_str)
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
raise ImportError(f"Could not import module '{module_str}': {e}")
|
|
42
|
+
|
|
43
|
+
# Get attribute
|
|
44
|
+
if not hasattr(module, attr_str):
|
|
45
|
+
raise AttributeError(
|
|
46
|
+
f"Module '{module_str}' has no attribute '{attr_str}'. "
|
|
47
|
+
f"Please ensure you have '{attr_str} = MarchAgentApp(...)' in your file."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
attr = getattr(module, attr_str)
|
|
51
|
+
return module, attr_str, attr
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_agent(import_str: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Load and run an agent using uvicorn-style import string.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
import_str: Import string like "main:app" or "main"
|
|
60
|
+
"""
|
|
61
|
+
from march_agent import MarchAgentApp
|
|
62
|
+
|
|
63
|
+
print(f"Loading agent from: {import_str}")
|
|
64
|
+
|
|
65
|
+
# Import the app
|
|
66
|
+
module, attr_name, app = import_from_string(import_str)
|
|
67
|
+
|
|
68
|
+
# Validate it's a MarchAgentApp
|
|
69
|
+
if not isinstance(app, MarchAgentApp):
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f"Attribute '{attr_name}' must be a MarchAgentApp instance, "
|
|
72
|
+
f"got {type(app).__name__} instead."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
print(f"Running {attr_name}.run()...")
|
|
76
|
+
app.run()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> None:
|
|
80
|
+
"""Main CLI entry point."""
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="march-agent",
|
|
83
|
+
description="March AI Agent CLI - Run AI agents with uvicorn-style imports",
|
|
84
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
85
|
+
epilog="""
|
|
86
|
+
Examples:
|
|
87
|
+
march-agent run main:app Run 'app' from 'main' module (recommended)
|
|
88
|
+
march-agent run main Run 'app' from 'main' module (defaults to :app)
|
|
89
|
+
march-agent run myagent:application Run 'application' from 'myagent' module
|
|
90
|
+
|
|
91
|
+
Your agent file should have:
|
|
92
|
+
app = MarchAgentApp(...)
|
|
93
|
+
|
|
94
|
+
@app.agent(name="...", about="...", document="...")
|
|
95
|
+
def my_agent(message):
|
|
96
|
+
# Handle message
|
|
97
|
+
""",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
101
|
+
|
|
102
|
+
# run command
|
|
103
|
+
run_parser = subparsers.add_parser(
|
|
104
|
+
"run",
|
|
105
|
+
help="Run an agent",
|
|
106
|
+
description="Load and run a March AI agent using module:attribute syntax",
|
|
107
|
+
)
|
|
108
|
+
run_parser.add_argument(
|
|
109
|
+
"app",
|
|
110
|
+
help="Import path in format 'module:attribute' (e.g., main:app)",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# version command
|
|
114
|
+
version_parser = subparsers.add_parser(
|
|
115
|
+
"version",
|
|
116
|
+
help="Show version information",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
# Handle commands
|
|
122
|
+
if args.command == "run":
|
|
123
|
+
try:
|
|
124
|
+
run_agent(args.app)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
elif args.command == "version":
|
|
130
|
+
from march_agent import __version__
|
|
131
|
+
print(f"march-agent version {__version__}")
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
parser.print_help()
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Conversation context for accessing message history."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
from .conversation_client import ConversationClient
|
|
7
|
+
from .conversation_message import ConversationMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Conversation:
|
|
11
|
+
"""Lazy-loaded conversation context (async)."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
conversation_id: str,
|
|
16
|
+
client: ConversationClient,
|
|
17
|
+
agent_name: Optional[str] = None,
|
|
18
|
+
):
|
|
19
|
+
self.id = conversation_id
|
|
20
|
+
self._client = client
|
|
21
|
+
self._agent_name = agent_name
|
|
22
|
+
|
|
23
|
+
async def get_history(
|
|
24
|
+
self,
|
|
25
|
+
role: Optional[str] = None,
|
|
26
|
+
limit: int = 100,
|
|
27
|
+
offset: int = 0,
|
|
28
|
+
) -> List[ConversationMessage]:
|
|
29
|
+
"""Fetch conversation message history.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
role: Optional filter by role ('user', 'assistant', 'system')
|
|
33
|
+
limit: Maximum messages to fetch
|
|
34
|
+
offset: Pagination offset
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of ConversationMessage objects
|
|
38
|
+
"""
|
|
39
|
+
messages = await self._client.get_messages(
|
|
40
|
+
conversation_id=self.id, role=role, limit=limit, offset=offset
|
|
41
|
+
)
|
|
42
|
+
return ConversationMessage.from_list(messages)
|
|
43
|
+
|
|
44
|
+
async def get_metadata(self) -> Dict[str, Any]:
|
|
45
|
+
"""Fetch conversation metadata."""
|
|
46
|
+
return await self._client.get_conversation(self.id)
|
|
47
|
+
|
|
48
|
+
async def get_agent_history(
|
|
49
|
+
self,
|
|
50
|
+
agent_name: Optional[str] = None,
|
|
51
|
+
role: Optional[str] = None,
|
|
52
|
+
limit: int = 100,
|
|
53
|
+
offset: int = 0,
|
|
54
|
+
) -> List[ConversationMessage]:
|
|
55
|
+
"""Fetch messages where agent was sender or recipient.
|
|
56
|
+
|
|
57
|
+
Makes two API calls concurrently to get both sent and received messages,
|
|
58
|
+
then merges and sorts by sequence number.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
agent_name: The agent name to filter by. Defaults to the current agent
|
|
62
|
+
if not specified (requires agent_name to be set during Conversation init).
|
|
63
|
+
role: Optional role filter ('user', 'assistant', 'system')
|
|
64
|
+
limit: Maximum messages to fetch per query
|
|
65
|
+
offset: Pagination offset
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of ConversationMessage where from_==agent_name OR to_==agent_name
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If agent_name is not provided and no default is set.
|
|
72
|
+
"""
|
|
73
|
+
# Use provided agent_name or fall back to the default from init
|
|
74
|
+
agent_name = agent_name or self._agent_name
|
|
75
|
+
if not agent_name:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"agent_name must be provided either as argument or set during Conversation initialization"
|
|
78
|
+
)
|
|
79
|
+
# Run both queries concurrently
|
|
80
|
+
sent_task = self._client.get_messages(
|
|
81
|
+
conversation_id=self.id,
|
|
82
|
+
role=role,
|
|
83
|
+
from_=agent_name,
|
|
84
|
+
limit=limit,
|
|
85
|
+
offset=offset,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
received_task = self._client.get_messages(
|
|
89
|
+
conversation_id=self.id,
|
|
90
|
+
role=role,
|
|
91
|
+
to_=agent_name,
|
|
92
|
+
limit=limit,
|
|
93
|
+
offset=offset,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
sent, received = await asyncio.gather(sent_task, received_task)
|
|
97
|
+
|
|
98
|
+
# Merge and dedupe (in case from_==to_)
|
|
99
|
+
all_msgs = {msg["id"]: msg for msg in sent + received}
|
|
100
|
+
|
|
101
|
+
# Sort by sequence_number and convert to typed messages
|
|
102
|
+
sorted_msgs = sorted(all_msgs.values(), key=lambda m: m.get("sequence_number", 0))
|
|
103
|
+
return ConversationMessage.from_list(sorted_msgs)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Conversation store client for fetching conversation history."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from .exceptions import APIException
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConversationClient:
|
|
13
|
+
"""Client for interacting with conversation-store API (async)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, base_url: str):
|
|
16
|
+
self.base_url = base_url.rstrip("/")
|
|
17
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
18
|
+
|
|
19
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
20
|
+
"""Get or create aiohttp session."""
|
|
21
|
+
if self._session is None or self._session.closed:
|
|
22
|
+
timeout = aiohttp.ClientTimeout(total=10.0)
|
|
23
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
|
24
|
+
return self._session
|
|
25
|
+
|
|
26
|
+
async def close(self):
|
|
27
|
+
"""Close the aiohttp session."""
|
|
28
|
+
if self._session and not self._session.closed:
|
|
29
|
+
await self._session.close()
|
|
30
|
+
|
|
31
|
+
async def get_conversation(self, conversation_id: str) -> Dict[str, Any]:
|
|
32
|
+
"""Get conversation metadata."""
|
|
33
|
+
url = f"{self.base_url}/conversations/{conversation_id}"
|
|
34
|
+
session = await self._get_session()
|
|
35
|
+
try:
|
|
36
|
+
async with session.get(url) as response:
|
|
37
|
+
if response.status == 404:
|
|
38
|
+
raise APIException(f"Conversation {conversation_id} not found")
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return await response.json()
|
|
41
|
+
except aiohttp.ClientError as e:
|
|
42
|
+
raise APIException(f"Failed to fetch conversation: {e}")
|
|
43
|
+
|
|
44
|
+
async def get_messages(
|
|
45
|
+
self,
|
|
46
|
+
conversation_id: str,
|
|
47
|
+
role: Optional[str] = None,
|
|
48
|
+
from_: Optional[str] = None,
|
|
49
|
+
to_: Optional[str] = None,
|
|
50
|
+
limit: int = 100,
|
|
51
|
+
offset: int = 0,
|
|
52
|
+
) -> List[Dict[str, Any]]:
|
|
53
|
+
"""Get messages from a conversation."""
|
|
54
|
+
url = f"{self.base_url}/conversations/{conversation_id}/messages"
|
|
55
|
+
params = {"limit": min(limit, 1000), "offset": offset}
|
|
56
|
+
if role:
|
|
57
|
+
params["role"] = role
|
|
58
|
+
if from_:
|
|
59
|
+
params["from"] = from_
|
|
60
|
+
if to_:
|
|
61
|
+
params["to"] = to_
|
|
62
|
+
|
|
63
|
+
session = await self._get_session()
|
|
64
|
+
try:
|
|
65
|
+
async with session.get(url, params=params) as response:
|
|
66
|
+
if response.status == 404:
|
|
67
|
+
raise APIException(f"Conversation {conversation_id} not found")
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
return await response.json()
|
|
70
|
+
except aiohttp.ClientError as e:
|
|
71
|
+
raise APIException(f"Failed to fetch messages: {e}")
|
|
72
|
+
|
|
73
|
+
async def update_conversation(
|
|
74
|
+
self,
|
|
75
|
+
conversation_id: str,
|
|
76
|
+
data: Dict[str, Any],
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
"""Update conversation fields (PATCH)."""
|
|
79
|
+
url = f"{self.base_url}/conversations/{conversation_id}"
|
|
80
|
+
session = await self._get_session()
|
|
81
|
+
try:
|
|
82
|
+
async with session.patch(url, json=data) as response:
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
return await response.json()
|
|
85
|
+
except aiohttp.ClientError as e:
|
|
86
|
+
raise APIException(f"Failed to update conversation: {e}")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Typed message from conversation-store."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ConversationMessage:
|
|
9
|
+
"""A message from conversation-store.
|
|
10
|
+
|
|
11
|
+
Represents a stored message in a conversation with full metadata.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
conversation_id: str
|
|
16
|
+
role: str # "user", "assistant", "system"
|
|
17
|
+
content: str
|
|
18
|
+
sequence_number: int
|
|
19
|
+
from_: Optional[str] = None # Sender (agent name or "user")
|
|
20
|
+
to_: Optional[str] = None # Recipient
|
|
21
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
22
|
+
schema: Optional[Dict[str, Any]] = None
|
|
23
|
+
response_schema: Optional[Dict[str, Any]] = None
|
|
24
|
+
created_at: Optional[str] = None
|
|
25
|
+
updated_at: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ConversationMessage":
|
|
29
|
+
"""Create from conversation-store API response."""
|
|
30
|
+
return cls(
|
|
31
|
+
id=str(data.get("id", "")),
|
|
32
|
+
conversation_id=str(data.get("conversation_id", "")),
|
|
33
|
+
role=data.get("role", ""),
|
|
34
|
+
content=data.get("content", ""),
|
|
35
|
+
sequence_number=data.get("sequence_number", 0),
|
|
36
|
+
from_=data.get("from_"),
|
|
37
|
+
to_=data.get("to_"),
|
|
38
|
+
metadata=data.get("metadata"),
|
|
39
|
+
schema=data.get("schema"),
|
|
40
|
+
response_schema=data.get("response_schema"),
|
|
41
|
+
created_at=data.get("created_at"),
|
|
42
|
+
updated_at=data.get("updated_at"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_list(cls, data: List[Dict[str, Any]]) -> List["ConversationMessage"]:
|
|
47
|
+
"""Create list from conversation-store API response."""
|
|
48
|
+
return [cls.from_dict(item) for item in data]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Custom exceptions for March AI Agent framework."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MarchAgentError(Exception):
|
|
5
|
+
"""Base exception for all March Agent errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegistrationError(MarchAgentError):
|
|
10
|
+
"""Raised when agent registration fails."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KafkaError(MarchAgentError):
|
|
15
|
+
"""Raised when Kafka operations fail."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HeartbeatError(MarchAgentError):
|
|
20
|
+
"""Raised when heartbeat operations fail."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MessageHandlerError(MarchAgentError):
|
|
25
|
+
"""Raised when message handler execution fails."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigurationError(MarchAgentError):
|
|
30
|
+
"""Raised when configuration is invalid."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class APIException(MarchAgentError):
|
|
35
|
+
"""Raised when conversation-store API requests fail."""
|
|
36
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Extensions package for march_agent
|