meshagent-anthropic 0.21.0__py3-none-any.whl → 0.22.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.
- meshagent/anthropic/__init__.py +16 -4
- meshagent/anthropic/mcp.py +103 -0
- meshagent/anthropic/{tools/messages_adapter.py → messages_adapter.py} +225 -27
- meshagent/anthropic/proxy/proxy.py +1 -1
- meshagent/anthropic/tests/anthropic_live_test.py +156 -0
- meshagent/anthropic/tests/mcp_test.py +64 -0
- meshagent/anthropic/tests/messages_adapter_test.py +179 -0
- meshagent/anthropic/tests/openai_responses_stream_adapter_test.py +102 -0
- meshagent/anthropic/version.py +1 -1
- {meshagent_anthropic-0.21.0.dist-info → meshagent_anthropic-0.22.0.dist-info}/METADATA +4 -4
- meshagent_anthropic-0.22.0.dist-info/RECORD +16 -0
- meshagent/anthropic/tools/__init__.py +0 -11
- meshagent_anthropic-0.21.0.dist-info/RECORD +0 -12
- /meshagent/anthropic/{tools/openai_responses_stream_adapter.py → openai_responses_stream_adapter.py} +0 -0
- {meshagent_anthropic-0.21.0.dist-info → meshagent_anthropic-0.22.0.dist-info}/WHEEL +0 -0
- {meshagent_anthropic-0.21.0.dist-info → meshagent_anthropic-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {meshagent_anthropic-0.21.0.dist-info → meshagent_anthropic-0.22.0.dist-info}/top_level.txt +0 -0
meshagent/anthropic/__init__.py
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .messages_adapter import (
|
|
2
2
|
AnthropicMessagesAdapter,
|
|
3
3
|
AnthropicMessagesToolResponseAdapter,
|
|
4
|
-
AnthropicOpenAIResponsesStreamAdapter,
|
|
5
4
|
)
|
|
6
|
-
from .
|
|
5
|
+
from .mcp import (
|
|
6
|
+
MCPConfig,
|
|
7
|
+
MCPServer,
|
|
8
|
+
MCPTool,
|
|
9
|
+
MCPToolConfig,
|
|
10
|
+
MCPToolset,
|
|
11
|
+
MCPToolkitBuilder,
|
|
12
|
+
)
|
|
13
|
+
from .openai_responses_stream_adapter import AnthropicOpenAIResponsesStreamAdapter
|
|
7
14
|
|
|
8
15
|
__all__ = [
|
|
9
|
-
__version__,
|
|
10
16
|
AnthropicMessagesAdapter,
|
|
11
17
|
AnthropicMessagesToolResponseAdapter,
|
|
12
18
|
AnthropicOpenAIResponsesStreamAdapter,
|
|
19
|
+
MCPConfig,
|
|
20
|
+
MCPServer,
|
|
21
|
+
MCPTool,
|
|
22
|
+
MCPToolConfig,
|
|
23
|
+
MCPToolset,
|
|
24
|
+
MCPToolkitBuilder,
|
|
13
25
|
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from meshagent.tools import BaseTool, Toolkit, ToolkitBuilder, ToolkitConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# This module wraps Anthropic's official MCP connector support:
|
|
11
|
+
# https://platform.claude.com/docs/en/agents-and-tools/mcp-connector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MCP_CONNECTOR_BETA = "mcp-client-2025-11-20"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPServer(BaseModel):
|
|
18
|
+
"""Anthropic `mcp_servers` entry."""
|
|
19
|
+
|
|
20
|
+
type: Literal["url"] = "url"
|
|
21
|
+
url: str
|
|
22
|
+
name: str
|
|
23
|
+
authorization_token: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MCPToolConfig(BaseModel):
|
|
27
|
+
enabled: Optional[bool] = None
|
|
28
|
+
defer_loading: Optional[bool] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCPToolset(BaseModel):
|
|
32
|
+
"""Anthropic `tools` entry for MCP connector."""
|
|
33
|
+
|
|
34
|
+
type: Literal["mcp_toolset"] = "mcp_toolset"
|
|
35
|
+
mcp_server_name: str
|
|
36
|
+
default_config: Optional[MCPToolConfig] = None
|
|
37
|
+
configs: Optional[dict[str, MCPToolConfig]] = None
|
|
38
|
+
|
|
39
|
+
# Pass-through cache control, if desired.
|
|
40
|
+
cache_control: Optional[dict] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MCPConfig(ToolkitConfig):
|
|
44
|
+
"""MeshAgent toolkit config that injects MCP connector params.
|
|
45
|
+
|
|
46
|
+
This is intentionally modeled after the OpenAI adapter's MCP config pattern
|
|
47
|
+
(a toolkit config that can be provided via `tools=[...]` in chat messages),
|
|
48
|
+
but it produces Anthropic-specific request parameters: `mcp_servers` and
|
|
49
|
+
`mcp_toolset` entries.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
name: Literal["mcp"] = "mcp"
|
|
53
|
+
|
|
54
|
+
mcp_servers: list[MCPServer]
|
|
55
|
+
toolsets: Optional[list[MCPToolset]] = None
|
|
56
|
+
betas: list[str] = [MCP_CONNECTOR_BETA]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MCPTool(BaseTool):
|
|
60
|
+
"""Non-executable tool that augments the Anthropic request."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, *, config: MCPConfig):
|
|
63
|
+
super().__init__(name="mcp")
|
|
64
|
+
self.config = config
|
|
65
|
+
|
|
66
|
+
def apply(self, *, request: dict) -> None:
|
|
67
|
+
"""Mutate an Anthropic Messages request in-place."""
|
|
68
|
+
|
|
69
|
+
# Ensure we use the beta Messages API surface.
|
|
70
|
+
betas = request.setdefault("betas", [])
|
|
71
|
+
for b in self.config.betas:
|
|
72
|
+
if b not in betas:
|
|
73
|
+
betas.append(b)
|
|
74
|
+
|
|
75
|
+
toolsets = self.config.toolsets
|
|
76
|
+
if toolsets is None:
|
|
77
|
+
toolsets = [
|
|
78
|
+
MCPToolset(mcp_server_name=s.name) for s in self.config.mcp_servers
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Merge/dedupe servers by name.
|
|
82
|
+
existing_servers = request.setdefault("mcp_servers", [])
|
|
83
|
+
dedup: dict[str, dict] = {
|
|
84
|
+
s["name"]: s
|
|
85
|
+
for s in existing_servers
|
|
86
|
+
if isinstance(s, dict) and isinstance(s.get("name"), str)
|
|
87
|
+
}
|
|
88
|
+
for server in self.config.mcp_servers:
|
|
89
|
+
dedup[server.name] = server.model_dump(mode="json", exclude_none=True)
|
|
90
|
+
request["mcp_servers"] = list(dedup.values())
|
|
91
|
+
|
|
92
|
+
# Anthropic MCP toolsets live inside the top-level `tools` array.
|
|
93
|
+
tools = request.setdefault("tools", [])
|
|
94
|
+
for toolset in toolsets:
|
|
95
|
+
tools.append(toolset.model_dump(mode="json", exclude_none=True))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MCPToolkitBuilder(ToolkitBuilder):
|
|
99
|
+
def __init__(self):
|
|
100
|
+
super().__init__(name="mcp", type=MCPConfig)
|
|
101
|
+
|
|
102
|
+
async def make(self, *, room, model: str, config: MCPConfig) -> Toolkit:
|
|
103
|
+
return Toolkit(name="mcp", tools=[MCPTool(config=config)])
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from meshagent.agents.agent import AgentChatContext
|
|
4
4
|
from meshagent.api import RoomClient, RoomException, RemoteParticipant
|
|
5
|
-
from meshagent.tools import Toolkit, ToolContext, Tool
|
|
5
|
+
from meshagent.tools import Toolkit, ToolContext, Tool, BaseTool
|
|
6
6
|
from meshagent.api.messaging import (
|
|
7
7
|
Response,
|
|
8
8
|
LinkResponse,
|
|
@@ -21,8 +21,10 @@ import os
|
|
|
21
21
|
import logging
|
|
22
22
|
import re
|
|
23
23
|
import asyncio
|
|
24
|
+
import base64
|
|
24
25
|
|
|
25
26
|
from meshagent.anthropic.proxy import get_client, get_logging_httpx_client
|
|
27
|
+
from meshagent.anthropic.mcp import MCPTool as MCPConnectorTool
|
|
26
28
|
|
|
27
29
|
try:
|
|
28
30
|
from anthropic import APIStatusError
|
|
@@ -150,7 +152,51 @@ class AnthropicMessagesToolResponseAdapter(ToolResponseAdapter):
|
|
|
150
152
|
# Allow advanced tools to return pre-built Anthropic blocks.
|
|
151
153
|
return [{"role": "user", "content": response.outputs}]
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
tool_result_content: list[dict]
|
|
156
|
+
|
|
157
|
+
if isinstance(response, FileResponse):
|
|
158
|
+
mime_type = (response.mime_type or "").lower()
|
|
159
|
+
|
|
160
|
+
if mime_type == "image/jpg":
|
|
161
|
+
mime_type = "image/jpeg"
|
|
162
|
+
|
|
163
|
+
if mime_type.startswith("image/"):
|
|
164
|
+
allowed = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
|
165
|
+
if mime_type not in allowed:
|
|
166
|
+
output = f"{response.name} was returned as {response.mime_type}, which Anthropic does not accept as an image block"
|
|
167
|
+
tool_result_content = [_text_block(output)]
|
|
168
|
+
else:
|
|
169
|
+
tool_result_content = [
|
|
170
|
+
{
|
|
171
|
+
"type": "image",
|
|
172
|
+
"source": {
|
|
173
|
+
"type": "base64",
|
|
174
|
+
"media_type": mime_type,
|
|
175
|
+
"data": base64.b64encode(response.data).decode("utf-8"),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
elif mime_type == "application/pdf":
|
|
181
|
+
tool_result_content = [
|
|
182
|
+
{
|
|
183
|
+
"type": "document",
|
|
184
|
+
"title": response.name,
|
|
185
|
+
"source": {
|
|
186
|
+
"type": "base64",
|
|
187
|
+
"media_type": "application/pdf",
|
|
188
|
+
"data": base64.b64encode(response.data).decode("utf-8"),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
output = await self.to_plain_text(room=room, response=response)
|
|
195
|
+
tool_result_content = [_text_block(output)]
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
output = await self.to_plain_text(room=room, response=response)
|
|
199
|
+
tool_result_content = [_text_block(output)]
|
|
154
200
|
|
|
155
201
|
message = {
|
|
156
202
|
"role": "user",
|
|
@@ -158,7 +204,7 @@ class AnthropicMessagesToolResponseAdapter(ToolResponseAdapter):
|
|
|
158
204
|
{
|
|
159
205
|
"type": "tool_result",
|
|
160
206
|
"tool_use_id": tool_use_id,
|
|
161
|
-
"content":
|
|
207
|
+
"content": tool_result_content,
|
|
162
208
|
}
|
|
163
209
|
],
|
|
164
210
|
}
|
|
@@ -210,31 +256,81 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
210
256
|
) -> tuple[list[dict], Optional[str]]:
|
|
211
257
|
system = context.get_system_instructions()
|
|
212
258
|
|
|
259
|
+
def as_blocks(role: str, content: Any) -> dict:
|
|
260
|
+
if isinstance(content, str):
|
|
261
|
+
return {"role": role, "content": [_text_block(content)]}
|
|
262
|
+
if isinstance(content, list):
|
|
263
|
+
return {"role": role, "content": content}
|
|
264
|
+
return {"role": role, "content": [_text_block(str(content))]}
|
|
265
|
+
|
|
213
266
|
messages: list[dict] = []
|
|
267
|
+
pending_tool_use_ids: set[str] = set()
|
|
268
|
+
|
|
214
269
|
for m in context.messages:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
270
|
+
role = m.get("role")
|
|
271
|
+
if role not in {"user", "assistant"}:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
msg = as_blocks(role, m.get("content"))
|
|
275
|
+
|
|
276
|
+
# Anthropic requires that tool_result blocks appear in the *immediately next*
|
|
277
|
+
# user message after an assistant tool_use.
|
|
278
|
+
if pending_tool_use_ids:
|
|
279
|
+
if role == "assistant":
|
|
280
|
+
# Drop any assistant chatter that appears between tool_use and tool_result.
|
|
281
|
+
logger.warning(
|
|
282
|
+
"dropping assistant message between tool_use and tool_result"
|
|
220
283
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# role == user
|
|
287
|
+
content_blocks = msg.get("content") or []
|
|
288
|
+
tool_results = [
|
|
289
|
+
b
|
|
290
|
+
for b in content_blocks
|
|
291
|
+
if isinstance(b, dict) and b.get("type") == "tool_result"
|
|
292
|
+
]
|
|
293
|
+
tool_result_ids = {
|
|
294
|
+
b.get("tool_use_id") for b in tool_results if b.get("tool_use_id")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if not pending_tool_use_ids.issubset(tool_result_ids):
|
|
298
|
+
# If we can't satisfy the ordering contract, it's better to fail early
|
|
299
|
+
# with a clear error than to send an invalid request.
|
|
300
|
+
raise RoomException(
|
|
301
|
+
"invalid transcript: tool_use blocks must be followed by a user message "
|
|
302
|
+
"containing tool_result blocks for all tool_use ids"
|
|
228
303
|
)
|
|
229
304
|
|
|
305
|
+
pending_tool_use_ids.clear()
|
|
306
|
+
|
|
307
|
+
# Track tool_use ids introduced by assistant messages.
|
|
308
|
+
if role == "assistant":
|
|
309
|
+
content_blocks = msg.get("content") or []
|
|
310
|
+
for b in content_blocks:
|
|
311
|
+
if isinstance(b, dict) and b.get("type") == "tool_use":
|
|
312
|
+
tool_id = b.get("id")
|
|
313
|
+
if tool_id:
|
|
314
|
+
pending_tool_use_ids.add(tool_id)
|
|
315
|
+
|
|
316
|
+
messages.append(msg)
|
|
317
|
+
|
|
230
318
|
return messages, system
|
|
231
319
|
|
|
232
|
-
|
|
320
|
+
def _messages_api(self, *, client: Any, request: dict) -> Any:
|
|
321
|
+
# The MCP connector requires `client.beta.messages.*`.
|
|
322
|
+
if request.get("betas") is not None:
|
|
323
|
+
return client.beta.messages
|
|
324
|
+
return client.messages
|
|
325
|
+
|
|
326
|
+
async def _create_with_optional_headers(self, *, client: Any, request: dict) -> Any:
|
|
327
|
+
api = self._messages_api(client=client, request=request)
|
|
233
328
|
try:
|
|
234
|
-
return await
|
|
329
|
+
return await api.create(**request)
|
|
235
330
|
except TypeError:
|
|
236
|
-
|
|
237
|
-
|
|
331
|
+
request = dict(request)
|
|
332
|
+
request.pop("extra_headers", None)
|
|
333
|
+
return await api.create(**request)
|
|
238
334
|
|
|
239
335
|
async def _stream_message(
|
|
240
336
|
self,
|
|
@@ -255,9 +351,10 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
255
351
|
```
|
|
256
352
|
"""
|
|
257
353
|
|
|
258
|
-
|
|
354
|
+
api = self._messages_api(client=client, request=request)
|
|
355
|
+
stream_mgr = api.stream(**request)
|
|
259
356
|
|
|
260
|
-
async with stream:
|
|
357
|
+
async with stream_mgr as stream:
|
|
261
358
|
async for event in stream:
|
|
262
359
|
event_handler({"type": event.type, "event": _as_jsonable(event)})
|
|
263
360
|
|
|
@@ -267,6 +364,49 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
267
364
|
)
|
|
268
365
|
return final_message
|
|
269
366
|
|
|
367
|
+
def _split_toolkits(
|
|
368
|
+
self, *, toolkits: list[Toolkit]
|
|
369
|
+
) -> tuple[list[Toolkit], list[MCPConnectorTool]]:
|
|
370
|
+
"""Split toolkits into executable tools and request middleware tools."""
|
|
371
|
+
|
|
372
|
+
executable_toolkits: list[Toolkit] = []
|
|
373
|
+
middleware: list[MCPConnectorTool] = []
|
|
374
|
+
|
|
375
|
+
for toolkit in toolkits:
|
|
376
|
+
executable_tools: list[Tool] = []
|
|
377
|
+
|
|
378
|
+
for t in toolkit.tools:
|
|
379
|
+
if isinstance(t, MCPConnectorTool):
|
|
380
|
+
middleware.append(t)
|
|
381
|
+
elif isinstance(t, Tool):
|
|
382
|
+
executable_tools.append(t)
|
|
383
|
+
elif isinstance(t, BaseTool):
|
|
384
|
+
# Non-executable tool types are ignored.
|
|
385
|
+
continue
|
|
386
|
+
else:
|
|
387
|
+
raise RoomException(f"unsupported tool type {type(t)}")
|
|
388
|
+
|
|
389
|
+
if executable_tools:
|
|
390
|
+
executable_toolkits.append(
|
|
391
|
+
Toolkit(
|
|
392
|
+
name=toolkit.name,
|
|
393
|
+
title=getattr(toolkit, "title", None),
|
|
394
|
+
description=getattr(toolkit, "description", None),
|
|
395
|
+
thumbnail_url=getattr(toolkit, "thumbnail_url", None),
|
|
396
|
+
rules=getattr(toolkit, "rules", []),
|
|
397
|
+
tools=executable_tools,
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return executable_toolkits, middleware
|
|
402
|
+
|
|
403
|
+
def _apply_request_middleware(
|
|
404
|
+
self, *, request: dict, middleware: list[MCPConnectorTool]
|
|
405
|
+
) -> dict:
|
|
406
|
+
for m in middleware:
|
|
407
|
+
m.apply(request=request)
|
|
408
|
+
return request
|
|
409
|
+
|
|
270
410
|
async def next(
|
|
271
411
|
self,
|
|
272
412
|
*,
|
|
@@ -291,8 +431,10 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
291
431
|
|
|
292
432
|
try:
|
|
293
433
|
while True:
|
|
294
|
-
|
|
295
|
-
|
|
434
|
+
executable_toolkits, middleware = self._split_toolkits(
|
|
435
|
+
toolkits=toolkits
|
|
436
|
+
)
|
|
437
|
+
tool_bundle = MessagesToolBundle(toolkits=executable_toolkits)
|
|
296
438
|
|
|
297
439
|
messages, system = self._convert_messages(context=context)
|
|
298
440
|
|
|
@@ -312,16 +454,45 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
312
454
|
on_behalf_of.get_attribute("name")
|
|
313
455
|
)
|
|
314
456
|
|
|
457
|
+
message_options = dict(self._message_options or {})
|
|
458
|
+
|
|
459
|
+
tools_list: list[dict] = tool_bundle.to_json() or []
|
|
460
|
+
extra_tools = message_options.pop("tools", None)
|
|
461
|
+
if isinstance(extra_tools, list):
|
|
462
|
+
tools_list.extend(extra_tools)
|
|
463
|
+
|
|
315
464
|
request = {
|
|
316
465
|
"model": model,
|
|
317
466
|
"max_tokens": self._max_tokens,
|
|
318
467
|
"messages": messages,
|
|
319
468
|
"system": system,
|
|
320
|
-
"tools":
|
|
469
|
+
"tools": tools_list,
|
|
321
470
|
"extra_headers": extra_headers or None,
|
|
322
|
-
**
|
|
471
|
+
**message_options,
|
|
323
472
|
}
|
|
324
473
|
|
|
474
|
+
request = self._apply_request_middleware(
|
|
475
|
+
request=request,
|
|
476
|
+
middleware=middleware,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Normalize empty lists to None for Anthropic.
|
|
480
|
+
if (
|
|
481
|
+
isinstance(request.get("tools"), list)
|
|
482
|
+
and len(request["tools"]) == 0
|
|
483
|
+
):
|
|
484
|
+
request["tools"] = None
|
|
485
|
+
if (
|
|
486
|
+
isinstance(request.get("mcp_servers"), list)
|
|
487
|
+
and len(request["mcp_servers"]) == 0
|
|
488
|
+
):
|
|
489
|
+
request["mcp_servers"] = None
|
|
490
|
+
if (
|
|
491
|
+
isinstance(request.get("betas"), list)
|
|
492
|
+
and len(request["betas"]) == 0
|
|
493
|
+
):
|
|
494
|
+
request["betas"] = None
|
|
495
|
+
|
|
325
496
|
# remove None fields
|
|
326
497
|
request = {k: v for k, v in request.items() if v is not None}
|
|
327
498
|
|
|
@@ -336,7 +507,8 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
336
507
|
response_dict = _as_jsonable(final_message)
|
|
337
508
|
else:
|
|
338
509
|
response = await self._create_with_optional_headers(
|
|
339
|
-
client,
|
|
510
|
+
client=client,
|
|
511
|
+
request=request,
|
|
340
512
|
)
|
|
341
513
|
response_dict = _as_jsonable(response)
|
|
342
514
|
|
|
@@ -376,9 +548,35 @@ class AnthropicMessagesAdapter(LLMAdapter[dict]):
|
|
|
376
548
|
tasks.append(asyncio.create_task(do_tool(tool_use)))
|
|
377
549
|
|
|
378
550
|
results = await asyncio.gather(*tasks)
|
|
551
|
+
|
|
552
|
+
# Anthropic requires tool_result blocks for *all* tool_use ids to appear in the
|
|
553
|
+
# *immediately next* user message after the assistant tool_use message.
|
|
554
|
+
tool_result_blocks: list[dict] = []
|
|
555
|
+
trailing_messages: list[dict] = []
|
|
556
|
+
|
|
379
557
|
for msgs in results:
|
|
380
558
|
for msg in msgs:
|
|
381
|
-
|
|
559
|
+
if (
|
|
560
|
+
isinstance(msg, dict)
|
|
561
|
+
and msg.get("role") == "user"
|
|
562
|
+
and isinstance(msg.get("content"), list)
|
|
563
|
+
and all(
|
|
564
|
+
isinstance(b, dict)
|
|
565
|
+
and b.get("type") == "tool_result"
|
|
566
|
+
for b in msg["content"]
|
|
567
|
+
)
|
|
568
|
+
):
|
|
569
|
+
tool_result_blocks.extend(msg["content"])
|
|
570
|
+
else:
|
|
571
|
+
trailing_messages.append(msg)
|
|
572
|
+
|
|
573
|
+
if tool_result_blocks:
|
|
574
|
+
context.messages.append(
|
|
575
|
+
{"role": "user", "content": tool_result_blocks}
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
for msg in trailing_messages:
|
|
579
|
+
context.messages.append(msg)
|
|
382
580
|
|
|
383
581
|
continue
|
|
384
582
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from meshagent.anthropic.messages_adapter import AnthropicMessagesAdapter
|
|
7
|
+
from meshagent.anthropic.mcp import MCPConfig, MCPServer, MCPTool
|
|
8
|
+
from meshagent.agents.agent import AgentChatContext
|
|
9
|
+
from meshagent.tools import Toolkit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _import_real_anthropic_sdk():
|
|
13
|
+
"""Import the external `anthropic` SDK without shadowing.
|
|
14
|
+
|
|
15
|
+
If `pytest` is run from inside `.../meshagent/`, Python may resolve
|
|
16
|
+
`import anthropic` to the local `meshagent/anthropic` package directory.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
cwd = os.getcwd()
|
|
20
|
+
|
|
21
|
+
if os.path.isdir(os.path.join(cwd, "anthropic")):
|
|
22
|
+
sys.path = [p for p in sys.path if p not in ("", cwd)]
|
|
23
|
+
|
|
24
|
+
import importlib
|
|
25
|
+
|
|
26
|
+
mod = importlib.import_module("anthropic")
|
|
27
|
+
|
|
28
|
+
mod_file = getattr(mod, "__file__", "") or ""
|
|
29
|
+
if mod_file.endswith("/meshagent/anthropic/__init__.py"):
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"Imported local `meshagent/anthropic` instead of the Anthropic SDK. "
|
|
32
|
+
"Run pytest from the repo root."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return mod
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
a = _import_real_anthropic_sdk()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _DummyRoom:
|
|
42
|
+
# Adapter won't touch room when no tools.
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_live_anthropic_adapter_messages_create_if_key_set():
|
|
48
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
49
|
+
if not api_key:
|
|
50
|
+
pytest.skip("ANTHROPIC_API_KEY not set")
|
|
51
|
+
|
|
52
|
+
model = os.getenv("ANTHROPIC_TEST_MODEL", "claude-sonnet-4-5")
|
|
53
|
+
|
|
54
|
+
client = a.AsyncAnthropic(api_key=api_key)
|
|
55
|
+
adapter = AnthropicMessagesAdapter(model=model, client=client, max_tokens=64)
|
|
56
|
+
|
|
57
|
+
ctx = AgentChatContext(system_role=None)
|
|
58
|
+
ctx.append_user_message("Say hello in one word.")
|
|
59
|
+
|
|
60
|
+
text = await adapter.next(context=ctx, room=_DummyRoom(), toolkits=[])
|
|
61
|
+
|
|
62
|
+
assert isinstance(text, str)
|
|
63
|
+
assert len(text.strip()) > 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_live_anthropic_adapter_streaming_if_key_set():
|
|
68
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
69
|
+
if not api_key:
|
|
70
|
+
pytest.skip("ANTHROPIC_API_KEY not set")
|
|
71
|
+
|
|
72
|
+
model = os.getenv("ANTHROPIC_TEST_MODEL", "claude-sonnet-4-5")
|
|
73
|
+
|
|
74
|
+
client = a.AsyncAnthropic(api_key=api_key)
|
|
75
|
+
adapter = AnthropicMessagesAdapter(model=model, client=client, max_tokens=64)
|
|
76
|
+
|
|
77
|
+
ctx = AgentChatContext(system_role=None)
|
|
78
|
+
ctx.append_user_message("Count from 1 to 3.")
|
|
79
|
+
|
|
80
|
+
seen_types: list[str] = []
|
|
81
|
+
|
|
82
|
+
def handler(event: dict):
|
|
83
|
+
if isinstance(event, dict) and "type" in event:
|
|
84
|
+
seen_types.append(event["type"])
|
|
85
|
+
|
|
86
|
+
text = await adapter.next(
|
|
87
|
+
context=ctx,
|
|
88
|
+
room=_DummyRoom(),
|
|
89
|
+
toolkits=[],
|
|
90
|
+
event_handler=handler,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert isinstance(text, str)
|
|
94
|
+
assert len(text.strip()) > 0
|
|
95
|
+
# These are best-effort; event types depend on Anthropic SDK.
|
|
96
|
+
assert len(seen_types) > 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_live_anthropic_mcp_deepwiki_if_key_set():
|
|
101
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
102
|
+
if not api_key:
|
|
103
|
+
pytest.skip("ANTHROPIC_API_KEY not set")
|
|
104
|
+
|
|
105
|
+
model = os.getenv("ANTHROPIC_TEST_MODEL", "claude-sonnet-4-5")
|
|
106
|
+
|
|
107
|
+
client = a.AsyncAnthropic(api_key=api_key)
|
|
108
|
+
adapter = AnthropicMessagesAdapter(model=model, client=client, max_tokens=256)
|
|
109
|
+
|
|
110
|
+
ctx = AgentChatContext(system_role=None)
|
|
111
|
+
ctx.append_user_message(
|
|
112
|
+
"Use the DeepWiki MCP toolset and make at least one tool call. "
|
|
113
|
+
"Then reply with a one-sentence summary of what you learned."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
mcp_toolkit = Toolkit(
|
|
117
|
+
name="mcp",
|
|
118
|
+
tools=[
|
|
119
|
+
MCPTool(
|
|
120
|
+
config=MCPConfig(
|
|
121
|
+
mcp_servers=[
|
|
122
|
+
MCPServer(url="https://mcp.deepwiki.com/mcp", name="deepwiki")
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
seen_mcp_blocks = False
|
|
130
|
+
|
|
131
|
+
def handler(event: dict):
|
|
132
|
+
nonlocal seen_mcp_blocks
|
|
133
|
+
if not isinstance(event, dict):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Adapter forwards Anthropic SDK stream events:
|
|
137
|
+
# {"type": "content_block_start", "event": {...}}
|
|
138
|
+
if event.get("type") == "content_block_start":
|
|
139
|
+
payload = event.get("event") or {}
|
|
140
|
+
content_block = payload.get("content_block") or {}
|
|
141
|
+
if content_block.get("type") in {"mcp_tool_use", "mcp_tool_result"}:
|
|
142
|
+
seen_mcp_blocks = True
|
|
143
|
+
|
|
144
|
+
text = await adapter.next(
|
|
145
|
+
context=ctx,
|
|
146
|
+
room=_DummyRoom(),
|
|
147
|
+
toolkits=[mcp_toolkit],
|
|
148
|
+
event_handler=handler,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert isinstance(text, str)
|
|
152
|
+
assert len(text.strip()) > 0
|
|
153
|
+
|
|
154
|
+
# This asserts the connector actually engaged (best-effort, but should be stable
|
|
155
|
+
# for DeepWiki).
|
|
156
|
+
assert seen_mcp_blocks
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from meshagent.anthropic.mcp import MCPConfig, MCPServer, MCPTool, MCPToolset
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_mcp_tool_apply_injects_servers_toolsets_and_beta():
|
|
5
|
+
cfg = MCPConfig(
|
|
6
|
+
mcp_servers=[
|
|
7
|
+
MCPServer(url="https://mcp.example.com/sse", name="example-mcp"),
|
|
8
|
+
],
|
|
9
|
+
toolsets=[MCPToolset(mcp_server_name="example-mcp")],
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
tool = MCPTool(config=cfg)
|
|
13
|
+
request: dict = {"tools": []}
|
|
14
|
+
tool.apply(request=request)
|
|
15
|
+
|
|
16
|
+
assert request["betas"] == ["mcp-client-2025-11-20"]
|
|
17
|
+
assert request["mcp_servers"] == [
|
|
18
|
+
{
|
|
19
|
+
"type": "url",
|
|
20
|
+
"url": "https://mcp.example.com/sse",
|
|
21
|
+
"name": "example-mcp",
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
assert request["tools"][0]["type"] == "mcp_toolset"
|
|
26
|
+
assert request["tools"][0]["mcp_server_name"] == "example-mcp"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_mcp_tool_apply_dedupes_servers_by_name_and_preserves_existing():
|
|
30
|
+
cfg = MCPConfig(
|
|
31
|
+
mcp_servers=[
|
|
32
|
+
MCPServer(url="https://mcp.example.com/sse", name="example-mcp"),
|
|
33
|
+
MCPServer(url="https://mcp.other.com/sse", name="other"),
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
tool = MCPTool(config=cfg)
|
|
37
|
+
|
|
38
|
+
request: dict = {
|
|
39
|
+
"tools": [{"type": "tool", "name": "some_tool"}],
|
|
40
|
+
"mcp_servers": [
|
|
41
|
+
{
|
|
42
|
+
"type": "url",
|
|
43
|
+
"url": "https://old.example.com/sse",
|
|
44
|
+
"name": "example-mcp",
|
|
45
|
+
"authorization_token": "OLD",
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"betas": ["some-other-beta"],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
tool.apply(request=request)
|
|
52
|
+
|
|
53
|
+
# Keeps existing betas and appends MCP beta.
|
|
54
|
+
assert "some-other-beta" in request["betas"]
|
|
55
|
+
assert "mcp-client-2025-11-20" in request["betas"]
|
|
56
|
+
|
|
57
|
+
# Dedupes by name; cfg overwrites the existing server entry.
|
|
58
|
+
by_name = {s["name"]: s for s in request["mcp_servers"]}
|
|
59
|
+
assert set(by_name.keys()) == {"example-mcp", "other"}
|
|
60
|
+
assert by_name["example-mcp"]["url"] == "https://mcp.example.com/sse"
|
|
61
|
+
|
|
62
|
+
# If toolsets omitted, it creates one per server.
|
|
63
|
+
mcp_toolsets = [t for t in request["tools"] if t.get("type") == "mcp_toolset"]
|
|
64
|
+
assert {t["mcp_server_name"] for t in mcp_toolsets} == {"example-mcp", "other"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from meshagent.anthropic.messages_adapter import AnthropicMessagesAdapter
|
|
4
|
+
from meshagent.agents.agent import AgentChatContext
|
|
5
|
+
from meshagent.tools import Tool, Toolkit
|
|
6
|
+
from meshagent.api import RoomException
|
|
7
|
+
from meshagent.agents.adapter import ToolResponseAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _DummyParticipant:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.id = "p1"
|
|
13
|
+
|
|
14
|
+
def get_attribute(self, name: str):
|
|
15
|
+
if name == "name":
|
|
16
|
+
return "tester"
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _DummyRoom:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.local_participant = _DummyParticipant()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _AnyArgsTool(Tool):
|
|
26
|
+
def __init__(self, name: str):
|
|
27
|
+
super().__init__(
|
|
28
|
+
name=name,
|
|
29
|
+
input_schema={"type": "object", "additionalProperties": True},
|
|
30
|
+
description="test tool",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def execute(self, context, **kwargs):
|
|
34
|
+
return {"ok": True, "args": kwargs}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _ToolResultAdapter(ToolResponseAdapter):
|
|
38
|
+
async def to_plain_text(self, *, room, response):
|
|
39
|
+
return "ok"
|
|
40
|
+
|
|
41
|
+
async def create_messages(self, *, context, tool_call, room, response):
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"role": "user",
|
|
45
|
+
"content": [
|
|
46
|
+
{
|
|
47
|
+
"type": "tool_result",
|
|
48
|
+
"tool_use_id": tool_call["id"],
|
|
49
|
+
"content": [{"type": "text", "text": "ok"}],
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _FakeAdapter(AnthropicMessagesAdapter):
|
|
57
|
+
def __init__(self, responses: list[dict]):
|
|
58
|
+
super().__init__(client=object())
|
|
59
|
+
self._responses = responses
|
|
60
|
+
self._idx = 0
|
|
61
|
+
|
|
62
|
+
async def _create_with_optional_headers(self, *, client, request):
|
|
63
|
+
if self._idx >= len(self._responses):
|
|
64
|
+
raise AssertionError("unexpected extra request")
|
|
65
|
+
resp = self._responses[self._idx]
|
|
66
|
+
self._idx += 1
|
|
67
|
+
return resp
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_convert_messages_drops_assistant_between_tool_use_and_tool_result():
|
|
71
|
+
ctx = AgentChatContext(
|
|
72
|
+
system_role=None,
|
|
73
|
+
messages=[
|
|
74
|
+
{"role": "user", "content": "hi"},
|
|
75
|
+
{
|
|
76
|
+
"role": "assistant",
|
|
77
|
+
"content": [
|
|
78
|
+
{"type": "text", "text": "calling tool"},
|
|
79
|
+
{
|
|
80
|
+
"type": "tool_use",
|
|
81
|
+
"id": "toolu_1",
|
|
82
|
+
"name": "tool_a",
|
|
83
|
+
"input": {},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{"role": "assistant", "content": "stray assistant message"},
|
|
88
|
+
{
|
|
89
|
+
"role": "user",
|
|
90
|
+
"content": [
|
|
91
|
+
{
|
|
92
|
+
"type": "tool_result",
|
|
93
|
+
"tool_use_id": "toolu_1",
|
|
94
|
+
"content": [{"type": "text", "text": "ok"}],
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
adapter = AnthropicMessagesAdapter(client=object())
|
|
102
|
+
msgs, _system = adapter._convert_messages(context=ctx)
|
|
103
|
+
|
|
104
|
+
assert [m["role"] for m in msgs] == ["user", "assistant", "user"]
|
|
105
|
+
assert msgs[1]["content"][1]["type"] == "tool_use"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_convert_messages_raises_if_tool_result_not_immediately_next():
|
|
109
|
+
ctx = AgentChatContext(
|
|
110
|
+
system_role=None,
|
|
111
|
+
messages=[
|
|
112
|
+
{"role": "user", "content": "hi"},
|
|
113
|
+
{
|
|
114
|
+
"role": "assistant",
|
|
115
|
+
"content": [
|
|
116
|
+
{
|
|
117
|
+
"type": "tool_use",
|
|
118
|
+
"id": "toolu_1",
|
|
119
|
+
"name": "tool_a",
|
|
120
|
+
"input": {},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{"role": "user", "content": "not a tool_result"},
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
adapter = AnthropicMessagesAdapter(client=object())
|
|
129
|
+
|
|
130
|
+
with pytest.raises(RoomException):
|
|
131
|
+
adapter._convert_messages(context=ctx)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def test_next_batches_multiple_tool_results_into_single_user_message():
|
|
136
|
+
responses = [
|
|
137
|
+
{
|
|
138
|
+
"content": [
|
|
139
|
+
{"type": "text", "text": "calling tools"},
|
|
140
|
+
{"type": "tool_use", "id": "toolu_1", "name": "tool_a", "input": {}},
|
|
141
|
+
{"type": "tool_use", "id": "toolu_2", "name": "tool_b", "input": {}},
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{"content": [{"type": "text", "text": "done"}]},
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
adapter = _FakeAdapter(responses=responses)
|
|
148
|
+
ctx = AgentChatContext(system_role=None)
|
|
149
|
+
ctx.append_user_message("run tools")
|
|
150
|
+
|
|
151
|
+
toolkit = Toolkit(
|
|
152
|
+
name="test",
|
|
153
|
+
tools=[_AnyArgsTool("tool_a"), _AnyArgsTool("tool_b")],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
result = await adapter.next(
|
|
157
|
+
context=ctx,
|
|
158
|
+
room=_DummyRoom(),
|
|
159
|
+
toolkits=[toolkit],
|
|
160
|
+
tool_adapter=_ToolResultAdapter(),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert result == "done"
|
|
164
|
+
|
|
165
|
+
# Expect: user -> assistant(tool_use) -> user(tool_results batched) -> assistant(final)
|
|
166
|
+
assert [m["role"] for m in ctx.messages] == [
|
|
167
|
+
"user",
|
|
168
|
+
"assistant",
|
|
169
|
+
"user",
|
|
170
|
+
"assistant",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
tool_results_msg = ctx.messages[2]
|
|
174
|
+
assert tool_results_msg["role"] == "user"
|
|
175
|
+
assert len(tool_results_msg["content"]) == 2
|
|
176
|
+
assert {b["tool_use_id"] for b in tool_results_msg["content"]} == {
|
|
177
|
+
"toolu_1",
|
|
178
|
+
"toolu_2",
|
|
179
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from meshagent.anthropic.openai_responses_stream_adapter import (
|
|
5
|
+
AnthropicOpenAIResponsesStreamAdapter,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Event(BaseModel):
|
|
10
|
+
type: str
|
|
11
|
+
index: int | None = None
|
|
12
|
+
message: dict | None = None
|
|
13
|
+
content_block: dict | None = None
|
|
14
|
+
delta: dict | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _FinalMessage(BaseModel):
|
|
18
|
+
id: str = "msg_1"
|
|
19
|
+
usage: dict = {"input_tokens": 3, "output_tokens": 5}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _FakeStream:
|
|
23
|
+
def __init__(self, events: list[BaseModel], final: BaseModel):
|
|
24
|
+
self._events = events
|
|
25
|
+
self._final = final
|
|
26
|
+
|
|
27
|
+
async def __aenter__(self):
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def __aiter__(self):
|
|
34
|
+
async def gen():
|
|
35
|
+
for e in self._events:
|
|
36
|
+
yield e
|
|
37
|
+
|
|
38
|
+
return gen()
|
|
39
|
+
|
|
40
|
+
async def get_final_message(self):
|
|
41
|
+
return self._final
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _FakeMessages:
|
|
45
|
+
def __init__(self, stream: _FakeStream):
|
|
46
|
+
self._stream = stream
|
|
47
|
+
|
|
48
|
+
def stream(self, **kwargs):
|
|
49
|
+
return self._stream
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _FakeClient:
|
|
53
|
+
def __init__(self, stream: _FakeStream):
|
|
54
|
+
self.messages = _FakeMessages(stream)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_openai_responses_stream_emits_content_part_events():
|
|
59
|
+
events = [
|
|
60
|
+
_Event(type="message_start", message={"id": "msg_1", "model": "claude"}),
|
|
61
|
+
_Event(type="content_block_start", index=0, content_block={"type": "text"}),
|
|
62
|
+
_Event(
|
|
63
|
+
type="content_block_delta",
|
|
64
|
+
index=0,
|
|
65
|
+
delta={"type": "text_delta", "text": "hi"},
|
|
66
|
+
),
|
|
67
|
+
_Event(type="content_block_stop", index=0),
|
|
68
|
+
_Event(type="message_stop"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
stream = _FakeStream(events=events, final=_FinalMessage())
|
|
72
|
+
client = _FakeClient(stream)
|
|
73
|
+
|
|
74
|
+
adapter = AnthropicOpenAIResponsesStreamAdapter(client=client)
|
|
75
|
+
|
|
76
|
+
emitted: list[dict] = []
|
|
77
|
+
|
|
78
|
+
def handler(e: dict):
|
|
79
|
+
emitted.append(e)
|
|
80
|
+
|
|
81
|
+
await adapter._stream_message(
|
|
82
|
+
client=client,
|
|
83
|
+
request={"model": "x", "max_tokens": 5, "messages": []},
|
|
84
|
+
event_handler=handler,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
types = [e["type"] for e in emitted]
|
|
88
|
+
|
|
89
|
+
assert "response.created" in types
|
|
90
|
+
assert "response.output_item.added" in types
|
|
91
|
+
assert "response.content_part.added" in types
|
|
92
|
+
assert "response.output_text.delta" in types
|
|
93
|
+
assert "response.output_text.done" in types
|
|
94
|
+
assert "response.content_part.done" in types
|
|
95
|
+
assert "response.output_item.done" in types
|
|
96
|
+
assert "response.completed" in types
|
|
97
|
+
|
|
98
|
+
# Sanity-check completed response contains usage.
|
|
99
|
+
completed = next(e for e in emitted if e["type"] == "response.completed")
|
|
100
|
+
assert completed["response"]["usage"]["input_tokens"] == 3
|
|
101
|
+
assert completed["response"]["usage"]["output_tokens"] == 5
|
|
102
|
+
assert completed["response"]["usage"]["total_tokens"] == 8
|
meshagent/anthropic/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.22.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-anthropic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.0
|
|
4
4
|
Summary: Anthropic Building Blocks for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -11,9 +11,9 @@ Description-Content-Type: text/markdown
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: pytest~=8.4
|
|
13
13
|
Requires-Dist: pytest-asyncio~=0.26
|
|
14
|
-
Requires-Dist: meshagent-api~=0.
|
|
15
|
-
Requires-Dist: meshagent-agents~=0.
|
|
16
|
-
Requires-Dist: meshagent-tools~=0.
|
|
14
|
+
Requires-Dist: meshagent-api~=0.22.0
|
|
15
|
+
Requires-Dist: meshagent-agents~=0.22.0
|
|
16
|
+
Requires-Dist: meshagent-tools~=0.22.0
|
|
17
17
|
Requires-Dist: anthropic<1.0,>=0.25
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
meshagent/anthropic/__init__.py,sha256=2VOFYIp1DY2ed8YfstSB1kV8n7hSX72v8d7gapm9INE,542
|
|
2
|
+
meshagent/anthropic/mcp.py,sha256=9yDMOPo9wihMmPIpQUKXdAYDPTkZmCfRuW-OyWVPvLA,3218
|
|
3
|
+
meshagent/anthropic/messages_adapter.py,sha256=6YWg75iBFqhsbAgl5oqy6a6BzXkoFAu3chFmOt58jk0,22895
|
|
4
|
+
meshagent/anthropic/openai_responses_stream_adapter.py,sha256=ODtWusjnPDNedE_Nx5dTdEAhihpWmAXyEbYHuF42D8M,15763
|
|
5
|
+
meshagent/anthropic/version.py,sha256=0kk8efeJF41FZIYweGTJylbizaWrp9W3qN78RClCWIU,23
|
|
6
|
+
meshagent/anthropic/proxy/__init__.py,sha256=_E4wvJC90tmMnatDodkD0kIB8zsDZ4_zFd-5s86WQ8c,106
|
|
7
|
+
meshagent/anthropic/proxy/proxy.py,sha256=nQ_P2c3GWAbfQswlc11DTyIRJzZWaOm1Wg08HpCTl_k,2827
|
|
8
|
+
meshagent/anthropic/tests/anthropic_live_test.py,sha256=t8Uy8za0He4p4oXhnVFAx6vsuCVcFFthtwsiN-AwVT0,4571
|
|
9
|
+
meshagent/anthropic/tests/mcp_test.py,sha256=fT_dDiBNMraGOLtUuu2F4q3Yf4QPfZN-ypZq2iZTQoc,2140
|
|
10
|
+
meshagent/anthropic/tests/messages_adapter_test.py,sha256=BQV1zOB1xM1-wRuGyvmIdeMhubmgmZH_1-tQLFXZLi8,5265
|
|
11
|
+
meshagent/anthropic/tests/openai_responses_stream_adapter_test.py,sha256=WLkF5wSRYKV83n3GgIDoKI4EQdNJS6_EqC1_7RlMNHs,2842
|
|
12
|
+
meshagent_anthropic-0.22.0.dist-info/licenses/LICENSE,sha256=TRD-XzqhdrBbIpokiGa61wuDTBc_ElKoFP9HSNihODc,10255
|
|
13
|
+
meshagent_anthropic-0.22.0.dist-info/METADATA,sha256=wnobcioGD5KijDIKd7xUaqkL9L8MsA2gD8NNP7QYSxI,1595
|
|
14
|
+
meshagent_anthropic-0.22.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
meshagent_anthropic-0.22.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
16
|
+
meshagent_anthropic-0.22.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from .messages_adapter import (
|
|
2
|
-
AnthropicMessagesAdapter,
|
|
3
|
-
AnthropicMessagesToolResponseAdapter,
|
|
4
|
-
)
|
|
5
|
-
from .openai_responses_stream_adapter import AnthropicOpenAIResponsesStreamAdapter
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
AnthropicMessagesAdapter,
|
|
9
|
-
AnthropicMessagesToolResponseAdapter,
|
|
10
|
-
AnthropicOpenAIResponsesStreamAdapter,
|
|
11
|
-
]
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
meshagent/anthropic/__init__.py,sha256=aQVq1WNkmJRwZiV9i7lF_K2MHrTyOSDe3ewuXU7FOEg,318
|
|
2
|
-
meshagent/anthropic/version.py,sha256=-reNiXr25nUU7em7_IKJzimCI10W4nA8ouxAiOr4FaQ,23
|
|
3
|
-
meshagent/anthropic/proxy/__init__.py,sha256=_E4wvJC90tmMnatDodkD0kIB8zsDZ4_zFd-5s86WQ8c,106
|
|
4
|
-
meshagent/anthropic/proxy/proxy.py,sha256=FSVuTao0NyVQ6SVritDNFrL0bborUaMrXt2z-65Zk-4,2821
|
|
5
|
-
meshagent/anthropic/tools/__init__.py,sha256=FwuQWhftwumt_GZJL20kHMBfvb6zpe1Vy6JaSOw6e9E,319
|
|
6
|
-
meshagent/anthropic/tools/messages_adapter.py,sha256=28Op_g-nBD9yCP9wiXQCsxoktxf_J31Fzeyixqnpei4,14821
|
|
7
|
-
meshagent/anthropic/tools/openai_responses_stream_adapter.py,sha256=ODtWusjnPDNedE_Nx5dTdEAhihpWmAXyEbYHuF42D8M,15763
|
|
8
|
-
meshagent_anthropic-0.21.0.dist-info/licenses/LICENSE,sha256=TRD-XzqhdrBbIpokiGa61wuDTBc_ElKoFP9HSNihODc,10255
|
|
9
|
-
meshagent_anthropic-0.21.0.dist-info/METADATA,sha256=RI1gurw9ovXk-yKc5Ij1n4pxASEYOIN5VI_GbQXwqDo,1595
|
|
10
|
-
meshagent_anthropic-0.21.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
meshagent_anthropic-0.21.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
12
|
-
meshagent_anthropic-0.21.0.dist-info/RECORD,,
|
/meshagent/anthropic/{tools/openai_responses_stream_adapter.py → openai_responses_stream_adapter.py}
RENAMED
|
File without changes
|
|
File without changes
|
{meshagent_anthropic-0.21.0.dist-info → meshagent_anthropic-0.22.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|