evolv-agent-sdk 0.2.0__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.
- evolv_agent_sdk/__init__.py +285 -0
- evolv_agent_sdk/_bundled/.gitkeep +1 -0
- evolv_agent_sdk/_cli_version.py +3 -0
- evolv_agent_sdk/_errors.py +61 -0
- evolv_agent_sdk/_internal/__init__.py +1 -0
- evolv_agent_sdk/_internal/client.py +108 -0
- evolv_agent_sdk/_internal/message_parser.py +181 -0
- evolv_agent_sdk/_internal/query.py +547 -0
- evolv_agent_sdk/_internal/transport/__init__.py +68 -0
- evolv_agent_sdk/_internal/transport/subprocess_cli.py +712 -0
- evolv_agent_sdk/_version.py +3 -0
- evolv_agent_sdk/client.py +365 -0
- evolv_agent_sdk/py.typed +0 -0
- evolv_agent_sdk/query.py +99 -0
- evolv_agent_sdk/types.py +689 -0
- evolv_agent_sdk-0.2.0.dist-info/METADATA +250 -0
- evolv_agent_sdk-0.2.0.dist-info/RECORD +19 -0
- evolv_agent_sdk-0.2.0.dist-info/WHEEL +4 -0
- evolv_agent_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""evolv Agent SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from ._errors import (
|
|
8
|
+
CLIConnectionError,
|
|
9
|
+
CLIJSONDecodeError,
|
|
10
|
+
CLINotFoundError,
|
|
11
|
+
EvolvSDKError,
|
|
12
|
+
ProcessError,
|
|
13
|
+
)
|
|
14
|
+
from ._internal.transport import Transport
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
from .client import EvolvSDKClient
|
|
17
|
+
from .query import query
|
|
18
|
+
from .types import (
|
|
19
|
+
AgentDefinition,
|
|
20
|
+
AssistantMessage,
|
|
21
|
+
CanUseTool,
|
|
22
|
+
ContentBlock,
|
|
23
|
+
EvolvAgentOptions,
|
|
24
|
+
HookCallback,
|
|
25
|
+
HookContext,
|
|
26
|
+
HookEvent,
|
|
27
|
+
HookInput,
|
|
28
|
+
HookJSONOutput,
|
|
29
|
+
HookMatcher,
|
|
30
|
+
McpSdkServerConfig,
|
|
31
|
+
McpServerConfig,
|
|
32
|
+
Message,
|
|
33
|
+
PermissionMode,
|
|
34
|
+
PermissionResult,
|
|
35
|
+
PermissionResultAllow,
|
|
36
|
+
PermissionResultDeny,
|
|
37
|
+
PermissionUpdate,
|
|
38
|
+
PostToolUseHookInput,
|
|
39
|
+
PreToolUseHookInput,
|
|
40
|
+
ResultMessage,
|
|
41
|
+
SdkBeta,
|
|
42
|
+
SettingSource,
|
|
43
|
+
SyncHookJSONOutput,
|
|
44
|
+
SystemMessage,
|
|
45
|
+
TextBlock,
|
|
46
|
+
ThinkingBlock,
|
|
47
|
+
ToolPermissionContext,
|
|
48
|
+
ToolResultBlock,
|
|
49
|
+
ToolUseBlock,
|
|
50
|
+
UserMessage,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# MCP Server Support
|
|
54
|
+
|
|
55
|
+
T = TypeVar("T")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class SdkMcpTool(Generic[T]):
|
|
60
|
+
"""Definition for an SDK MCP tool."""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
description: str
|
|
64
|
+
input_schema: type[T] | dict[str, Any]
|
|
65
|
+
handler: Callable[[T], Awaitable[dict[str, Any]]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def tool(
|
|
69
|
+
name: str, description: str, input_schema: type | dict[str, Any]
|
|
70
|
+
) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
|
|
71
|
+
"""Decorator for defining MCP tools with type safety.
|
|
72
|
+
|
|
73
|
+
Creates a tool that can be used with SDK MCP servers. The tool runs
|
|
74
|
+
in-process within your Python application, providing better performance
|
|
75
|
+
than external MCP servers.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: Unique identifier for the tool.
|
|
79
|
+
description: Human-readable description of what the tool does.
|
|
80
|
+
input_schema: Schema defining the tool's input parameters.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A decorator function that wraps the tool implementation.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
```python
|
|
87
|
+
@tool("greet", "Greet a user", {"name": str})
|
|
88
|
+
async def greet(args):
|
|
89
|
+
return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
|
|
90
|
+
```
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def decorator(
|
|
94
|
+
handler: Callable[[Any], Awaitable[dict[str, Any]]],
|
|
95
|
+
) -> SdkMcpTool[Any]:
|
|
96
|
+
return SdkMcpTool(
|
|
97
|
+
name=name,
|
|
98
|
+
description=description,
|
|
99
|
+
input_schema=input_schema,
|
|
100
|
+
handler=handler,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_sdk_mcp_server(
|
|
107
|
+
name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None
|
|
108
|
+
) -> McpSdkServerConfig:
|
|
109
|
+
"""Create an in-process MCP server that runs within your Python application.
|
|
110
|
+
|
|
111
|
+
Unlike external MCP servers that run as separate processes, SDK MCP servers
|
|
112
|
+
run directly in your application's process.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
name: Unique identifier for the server.
|
|
116
|
+
version: Server version string. Defaults to "1.0.0".
|
|
117
|
+
tools: List of SdkMcpTool instances created with the @tool decorator.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
McpSdkServerConfig: A configuration object for use with EvolvAgentOptions.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
```python
|
|
124
|
+
@tool("add", "Add numbers", {"a": float, "b": float})
|
|
125
|
+
async def add(args):
|
|
126
|
+
return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}
|
|
127
|
+
|
|
128
|
+
calculator = create_sdk_mcp_server(
|
|
129
|
+
name="calculator",
|
|
130
|
+
tools=[add]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
options = EvolvAgentOptions(
|
|
134
|
+
mcp_servers={"calc": calculator},
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
"""
|
|
138
|
+
from mcp.server import Server
|
|
139
|
+
from mcp.types import ImageContent, TextContent, Tool
|
|
140
|
+
|
|
141
|
+
server = Server(name, version=version)
|
|
142
|
+
|
|
143
|
+
if tools:
|
|
144
|
+
tool_map = {tool_def.name: tool_def for tool_def in tools}
|
|
145
|
+
|
|
146
|
+
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
|
|
147
|
+
async def list_tools() -> list[Tool]:
|
|
148
|
+
"""Return the list of available tools."""
|
|
149
|
+
tool_list = []
|
|
150
|
+
for tool_def in tools:
|
|
151
|
+
if isinstance(tool_def.input_schema, dict):
|
|
152
|
+
if (
|
|
153
|
+
"type" in tool_def.input_schema
|
|
154
|
+
and "properties" in tool_def.input_schema
|
|
155
|
+
):
|
|
156
|
+
schema = tool_def.input_schema
|
|
157
|
+
else:
|
|
158
|
+
properties = {}
|
|
159
|
+
for param_name, param_type in tool_def.input_schema.items():
|
|
160
|
+
if param_type is str:
|
|
161
|
+
properties[param_name] = {"type": "string"}
|
|
162
|
+
elif param_type is int:
|
|
163
|
+
properties[param_name] = {"type": "integer"}
|
|
164
|
+
elif param_type is float:
|
|
165
|
+
properties[param_name] = {"type": "number"}
|
|
166
|
+
elif param_type is bool:
|
|
167
|
+
properties[param_name] = {"type": "boolean"}
|
|
168
|
+
else:
|
|
169
|
+
properties[param_name] = {"type": "string"}
|
|
170
|
+
schema = {
|
|
171
|
+
"type": "object",
|
|
172
|
+
"properties": properties,
|
|
173
|
+
"required": list(properties.keys()),
|
|
174
|
+
}
|
|
175
|
+
else:
|
|
176
|
+
schema = {"type": "object", "properties": {}}
|
|
177
|
+
|
|
178
|
+
tool_list.append(
|
|
179
|
+
Tool(
|
|
180
|
+
name=tool_def.name,
|
|
181
|
+
description=tool_def.description,
|
|
182
|
+
inputSchema=schema,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return tool_list
|
|
186
|
+
|
|
187
|
+
@server.call_tool() # type: ignore[untyped-decorator]
|
|
188
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
|
|
189
|
+
"""Execute a tool by name with given arguments."""
|
|
190
|
+
if name not in tool_map:
|
|
191
|
+
raise ValueError(f"Tool '{name}' not found")
|
|
192
|
+
|
|
193
|
+
tool_def = tool_map[name]
|
|
194
|
+
result = await tool_def.handler(arguments)
|
|
195
|
+
|
|
196
|
+
content: list[TextContent | ImageContent] = []
|
|
197
|
+
if "content" in result:
|
|
198
|
+
for item in result["content"]:
|
|
199
|
+
if item.get("type") == "text":
|
|
200
|
+
content.append(TextContent(type="text", text=item["text"]))
|
|
201
|
+
if item.get("type") == "image":
|
|
202
|
+
content.append(
|
|
203
|
+
ImageContent(
|
|
204
|
+
type="image",
|
|
205
|
+
data=item["data"],
|
|
206
|
+
mimeType=item["mimeType"],
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return content
|
|
211
|
+
|
|
212
|
+
return McpSdkServerConfig(type="sdk", name=name, instance=server)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Backward compatibility aliases
|
|
216
|
+
CortexSDKClient = EvolvSDKClient
|
|
217
|
+
CortexAgentOptions = EvolvAgentOptions
|
|
218
|
+
CortexSDKError = EvolvSDKError
|
|
219
|
+
ClaudeSDKClient = EvolvSDKClient
|
|
220
|
+
ClaudeAgentOptions = EvolvAgentOptions
|
|
221
|
+
ClaudeSDKError = EvolvSDKError
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = [
|
|
225
|
+
# Main exports
|
|
226
|
+
"query",
|
|
227
|
+
"__version__",
|
|
228
|
+
# Transport
|
|
229
|
+
"Transport",
|
|
230
|
+
"EvolvSDKClient",
|
|
231
|
+
# Types
|
|
232
|
+
"PermissionMode",
|
|
233
|
+
"McpServerConfig",
|
|
234
|
+
"McpSdkServerConfig",
|
|
235
|
+
"UserMessage",
|
|
236
|
+
"AssistantMessage",
|
|
237
|
+
"SystemMessage",
|
|
238
|
+
"ResultMessage",
|
|
239
|
+
"Message",
|
|
240
|
+
"EvolvAgentOptions",
|
|
241
|
+
"TextBlock",
|
|
242
|
+
"ThinkingBlock",
|
|
243
|
+
"ToolUseBlock",
|
|
244
|
+
"ToolResultBlock",
|
|
245
|
+
"ContentBlock",
|
|
246
|
+
# Tool callbacks
|
|
247
|
+
"CanUseTool",
|
|
248
|
+
"ToolPermissionContext",
|
|
249
|
+
"PermissionResult",
|
|
250
|
+
"PermissionResultAllow",
|
|
251
|
+
"PermissionResultDeny",
|
|
252
|
+
"PermissionUpdate",
|
|
253
|
+
# Hooks
|
|
254
|
+
"HookCallback",
|
|
255
|
+
"HookContext",
|
|
256
|
+
"HookEvent",
|
|
257
|
+
"HookInput",
|
|
258
|
+
"HookJSONOutput",
|
|
259
|
+
"HookMatcher",
|
|
260
|
+
"PreToolUseHookInput",
|
|
261
|
+
"PostToolUseHookInput",
|
|
262
|
+
"SyncHookJSONOutput",
|
|
263
|
+
# Agent support
|
|
264
|
+
"AgentDefinition",
|
|
265
|
+
"SettingSource",
|
|
266
|
+
# Beta support
|
|
267
|
+
"SdkBeta",
|
|
268
|
+
# MCP Server Support
|
|
269
|
+
"create_sdk_mcp_server",
|
|
270
|
+
"tool",
|
|
271
|
+
"SdkMcpTool",
|
|
272
|
+
# Errors
|
|
273
|
+
"EvolvSDKError",
|
|
274
|
+
"CLIConnectionError",
|
|
275
|
+
"CLINotFoundError",
|
|
276
|
+
"ProcessError",
|
|
277
|
+
"CLIJSONDecodeError",
|
|
278
|
+
# Backward compatibility aliases
|
|
279
|
+
"CortexSDKClient",
|
|
280
|
+
"CortexAgentOptions",
|
|
281
|
+
"CortexSDKError",
|
|
282
|
+
"ClaudeSDKClient",
|
|
283
|
+
"ClaudeAgentOptions",
|
|
284
|
+
"ClaudeSDKError",
|
|
285
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Placeholder for bundled CLI binary
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Error types for evolv Agent SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EvolvSDKError(Exception):
|
|
7
|
+
"""Base exception for all evolv Agent SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CLIConnectionError(EvolvSDKError):
|
|
11
|
+
"""Raised when unable to connect to evolv Code."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CLINotFoundError(CLIConnectionError):
|
|
15
|
+
"""Raised when evolv Code is not found or not installed."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, message: str = "evolv Code not found", cli_path: str | None = None
|
|
19
|
+
):
|
|
20
|
+
if cli_path:
|
|
21
|
+
message = f"{message}: {cli_path}"
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProcessError(EvolvSDKError):
|
|
26
|
+
"""Raised when the CLI process fails."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self, message: str, exit_code: int | None = None, stderr: str | None = None
|
|
30
|
+
):
|
|
31
|
+
self.exit_code = exit_code
|
|
32
|
+
self.stderr = stderr
|
|
33
|
+
|
|
34
|
+
if exit_code is not None:
|
|
35
|
+
message = f"{message} (exit code: {exit_code})"
|
|
36
|
+
if stderr:
|
|
37
|
+
message = f"{message}\nError output: {stderr}"
|
|
38
|
+
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CLIJSONDecodeError(EvolvSDKError):
|
|
43
|
+
"""Raised when unable to decode JSON from CLI output."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, line: str, original_error: Exception):
|
|
46
|
+
self.line = line
|
|
47
|
+
self.original_error = original_error
|
|
48
|
+
super().__init__(f"Failed to decode JSON: {line[:100]}...")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MessageParseError(EvolvSDKError):
|
|
52
|
+
"""Raised when unable to parse a message from CLI output."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, data: dict[str, Any] | None = None):
|
|
55
|
+
self.data = data
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Backward compatibility aliases
|
|
60
|
+
CortexSDKError = EvolvSDKError
|
|
61
|
+
ClaudeSDKError = EvolvSDKError
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal modules for evolv Agent SDK."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Internal client implementation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..types import (
|
|
8
|
+
EvolvAgentOptions,
|
|
9
|
+
HookEvent,
|
|
10
|
+
HookMatcher,
|
|
11
|
+
Message,
|
|
12
|
+
)
|
|
13
|
+
from .message_parser import parse_message
|
|
14
|
+
from .query import Query
|
|
15
|
+
from .transport import Transport
|
|
16
|
+
from .transport.subprocess_cli import SubprocessCLITransport
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InternalClient:
|
|
20
|
+
"""Internal client implementation."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize the internal client."""
|
|
24
|
+
|
|
25
|
+
def _convert_hooks_to_internal_format(
|
|
26
|
+
self, hooks: dict[HookEvent, list[HookMatcher]]
|
|
27
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
28
|
+
"""Convert HookMatcher format to internal Query format."""
|
|
29
|
+
internal_hooks: dict[str, list[dict[str, Any]]] = {}
|
|
30
|
+
for event, matchers in hooks.items():
|
|
31
|
+
internal_hooks[event] = []
|
|
32
|
+
for matcher in matchers:
|
|
33
|
+
internal_matcher: dict[str, Any] = {
|
|
34
|
+
"matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
|
|
35
|
+
"hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
|
|
36
|
+
}
|
|
37
|
+
if hasattr(matcher, "timeout") and matcher.timeout is not None:
|
|
38
|
+
internal_matcher["timeout"] = matcher.timeout
|
|
39
|
+
internal_hooks[event].append(internal_matcher)
|
|
40
|
+
return internal_hooks
|
|
41
|
+
|
|
42
|
+
async def process_query(
|
|
43
|
+
self,
|
|
44
|
+
prompt: str | AsyncIterable[dict[str, Any]],
|
|
45
|
+
options: EvolvAgentOptions,
|
|
46
|
+
transport: Transport | None = None,
|
|
47
|
+
) -> AsyncIterator[Message]:
|
|
48
|
+
"""Process a query through transport and Query."""
|
|
49
|
+
|
|
50
|
+
configured_options = options
|
|
51
|
+
if options.can_use_tool:
|
|
52
|
+
if isinstance(prompt, str):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"can_use_tool callback requires streaming mode. "
|
|
55
|
+
"Please provide prompt as an AsyncIterable instead of a string."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if options.permission_prompt_tool_name:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"can_use_tool callback cannot be used with permission_prompt_tool_name. "
|
|
61
|
+
"Please use one or the other."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
configured_options = replace(options, permission_prompt_tool_name="stdio")
|
|
65
|
+
|
|
66
|
+
if transport is not None:
|
|
67
|
+
chosen_transport = transport
|
|
68
|
+
else:
|
|
69
|
+
chosen_transport = SubprocessCLITransport(
|
|
70
|
+
prompt=prompt,
|
|
71
|
+
options=configured_options,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
await chosen_transport.connect()
|
|
75
|
+
|
|
76
|
+
sdk_mcp_servers = {}
|
|
77
|
+
if configured_options.mcp_servers and isinstance(
|
|
78
|
+
configured_options.mcp_servers, dict
|
|
79
|
+
):
|
|
80
|
+
for name, config in configured_options.mcp_servers.items():
|
|
81
|
+
if isinstance(config, dict) and config.get("type") == "sdk":
|
|
82
|
+
sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item]
|
|
83
|
+
|
|
84
|
+
is_streaming = not isinstance(prompt, str)
|
|
85
|
+
query = Query(
|
|
86
|
+
transport=chosen_transport,
|
|
87
|
+
is_streaming_mode=is_streaming,
|
|
88
|
+
can_use_tool=configured_options.can_use_tool,
|
|
89
|
+
hooks=self._convert_hooks_to_internal_format(configured_options.hooks)
|
|
90
|
+
if configured_options.hooks
|
|
91
|
+
else None,
|
|
92
|
+
sdk_mcp_servers=sdk_mcp_servers,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await query.start()
|
|
97
|
+
|
|
98
|
+
if is_streaming:
|
|
99
|
+
await query.initialize()
|
|
100
|
+
|
|
101
|
+
if isinstance(prompt, AsyncIterable) and query._tg:
|
|
102
|
+
query._tg.start_soon(query.stream_input, prompt)
|
|
103
|
+
|
|
104
|
+
async for data in query.receive_messages():
|
|
105
|
+
yield parse_message(data)
|
|
106
|
+
|
|
107
|
+
finally:
|
|
108
|
+
await query.close()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Message parser for evolv Agent SDK responses."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .._errors import MessageParseError
|
|
7
|
+
from ..types import (
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
ContentBlock,
|
|
10
|
+
Message,
|
|
11
|
+
ResultMessage,
|
|
12
|
+
StreamEvent,
|
|
13
|
+
SystemMessage,
|
|
14
|
+
TextBlock,
|
|
15
|
+
ThinkingBlock,
|
|
16
|
+
ToolResultBlock,
|
|
17
|
+
ToolUseBlock,
|
|
18
|
+
UserMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_message(data: dict[str, Any]) -> Message:
|
|
25
|
+
"""
|
|
26
|
+
Parse message from CLI output into typed Message objects.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Raw message dictionary from CLI output
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Parsed Message object
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
MessageParseError: If parsing fails or message type is unrecognized
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
raise MessageParseError(
|
|
39
|
+
f"Invalid message data type (expected dict, got {type(data).__name__})",
|
|
40
|
+
data,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
message_type = data.get("type")
|
|
44
|
+
if not message_type:
|
|
45
|
+
raise MessageParseError("Message missing 'type' field", data)
|
|
46
|
+
|
|
47
|
+
match message_type:
|
|
48
|
+
case "user":
|
|
49
|
+
try:
|
|
50
|
+
parent_tool_use_id = data.get("parent_tool_use_id")
|
|
51
|
+
tool_use_result = data.get("tool_use_result")
|
|
52
|
+
uuid = data.get("uuid")
|
|
53
|
+
if isinstance(data["message"]["content"], list):
|
|
54
|
+
user_content_blocks: list[ContentBlock] = []
|
|
55
|
+
for block in data["message"]["content"]:
|
|
56
|
+
match block["type"]:
|
|
57
|
+
case "text":
|
|
58
|
+
user_content_blocks.append(
|
|
59
|
+
TextBlock(text=block["text"])
|
|
60
|
+
)
|
|
61
|
+
case "tool_use":
|
|
62
|
+
user_content_blocks.append(
|
|
63
|
+
ToolUseBlock(
|
|
64
|
+
id=block["id"],
|
|
65
|
+
name=block["name"],
|
|
66
|
+
input=block["input"],
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
case "tool_result":
|
|
70
|
+
user_content_blocks.append(
|
|
71
|
+
ToolResultBlock(
|
|
72
|
+
tool_use_id=block["tool_use_id"],
|
|
73
|
+
content=block.get("content"),
|
|
74
|
+
is_error=block.get("is_error"),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return UserMessage(
|
|
78
|
+
content=user_content_blocks,
|
|
79
|
+
uuid=uuid,
|
|
80
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
81
|
+
tool_use_result=tool_use_result,
|
|
82
|
+
)
|
|
83
|
+
return UserMessage(
|
|
84
|
+
content=data["message"]["content"],
|
|
85
|
+
uuid=uuid,
|
|
86
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
87
|
+
tool_use_result=tool_use_result,
|
|
88
|
+
)
|
|
89
|
+
except KeyError as e:
|
|
90
|
+
raise MessageParseError(
|
|
91
|
+
f"Missing required field in user message: {e}", data
|
|
92
|
+
) from e
|
|
93
|
+
|
|
94
|
+
case "assistant":
|
|
95
|
+
try:
|
|
96
|
+
content_blocks: list[ContentBlock] = []
|
|
97
|
+
for block in data["message"]["content"]:
|
|
98
|
+
match block["type"]:
|
|
99
|
+
case "text":
|
|
100
|
+
content_blocks.append(TextBlock(text=block["text"]))
|
|
101
|
+
case "thinking":
|
|
102
|
+
# evolv might not provide signature, use empty string
|
|
103
|
+
content_blocks.append(
|
|
104
|
+
ThinkingBlock(
|
|
105
|
+
thinking=block["thinking"],
|
|
106
|
+
signature=block.get("signature", ""),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
case "tool_use":
|
|
110
|
+
content_blocks.append(
|
|
111
|
+
ToolUseBlock(
|
|
112
|
+
id=block["id"],
|
|
113
|
+
name=block["name"],
|
|
114
|
+
input=block["input"],
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
case "tool_result":
|
|
118
|
+
content_blocks.append(
|
|
119
|
+
ToolResultBlock(
|
|
120
|
+
tool_use_id=block["tool_use_id"],
|
|
121
|
+
content=block.get("content"),
|
|
122
|
+
is_error=block.get("is_error"),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return AssistantMessage(
|
|
127
|
+
content=content_blocks,
|
|
128
|
+
model=data["message"].get("model", "evolv"),
|
|
129
|
+
parent_tool_use_id=data.get("parent_tool_use_id"),
|
|
130
|
+
error=data.get("error"),
|
|
131
|
+
)
|
|
132
|
+
except KeyError as e:
|
|
133
|
+
raise MessageParseError(
|
|
134
|
+
f"Missing required field in assistant message: {e}", data
|
|
135
|
+
) from e
|
|
136
|
+
|
|
137
|
+
case "system":
|
|
138
|
+
try:
|
|
139
|
+
return SystemMessage(
|
|
140
|
+
subtype=data["subtype"],
|
|
141
|
+
data=data,
|
|
142
|
+
)
|
|
143
|
+
except KeyError as e:
|
|
144
|
+
raise MessageParseError(
|
|
145
|
+
f"Missing required field in system message: {e}", data
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
case "result":
|
|
149
|
+
try:
|
|
150
|
+
return ResultMessage(
|
|
151
|
+
subtype=data["subtype"],
|
|
152
|
+
duration_ms=data.get("duration_ms", 0),
|
|
153
|
+
duration_api_ms=data.get("duration_api_ms", 0),
|
|
154
|
+
is_error=data.get("is_error", data.get("subtype") != "success"),
|
|
155
|
+
num_turns=data.get("num_turns", 1),
|
|
156
|
+
session_id=data.get("session_id", "evolv-session"),
|
|
157
|
+
total_cost_usd=data.get("total_cost_usd", 0),
|
|
158
|
+
usage=data.get("usage"),
|
|
159
|
+
result=data.get("result"),
|
|
160
|
+
structured_output=data.get("structured_output"),
|
|
161
|
+
)
|
|
162
|
+
except KeyError as e:
|
|
163
|
+
raise MessageParseError(
|
|
164
|
+
f"Missing required field in result message: {e}", data
|
|
165
|
+
) from e
|
|
166
|
+
|
|
167
|
+
case "stream_event":
|
|
168
|
+
try:
|
|
169
|
+
return StreamEvent(
|
|
170
|
+
uuid=data["uuid"],
|
|
171
|
+
session_id=data["session_id"],
|
|
172
|
+
event=data["event"],
|
|
173
|
+
parent_tool_use_id=data.get("parent_tool_use_id"),
|
|
174
|
+
)
|
|
175
|
+
except KeyError as e:
|
|
176
|
+
raise MessageParseError(
|
|
177
|
+
f"Missing required field in stream_event message: {e}", data
|
|
178
|
+
) from e
|
|
179
|
+
|
|
180
|
+
case _:
|
|
181
|
+
raise MessageParseError(f"Unknown message type: {message_type}", data)
|