mcp-mesh 0.6.0__py3-none-any.whl → 0.6.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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/decorator_registry.py +26 -2
- _mcp_mesh/engine/dependency_injector.py +14 -1
- _mcp_mesh/engine/llm_config.py +11 -7
- _mcp_mesh/engine/mesh_llm_agent.py +247 -61
- _mcp_mesh/engine/mesh_llm_agent_injector.py +130 -0
- _mcp_mesh/engine/provider_handlers/__init__.py +20 -0
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +122 -0
- _mcp_mesh/engine/provider_handlers/claude_handler.py +138 -0
- _mcp_mesh/engine/provider_handlers/generic_handler.py +156 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +163 -0
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +167 -0
- _mcp_mesh/engine/response_parser.py +3 -38
- _mcp_mesh/engine/tool_schema_builder.py +3 -2
- _mcp_mesh/generated/.openapi-generator/FILES +3 -0
- _mcp_mesh/generated/.openapi-generator-ignore +0 -1
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +51 -97
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +42 -72
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +11 -1
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +108 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +95 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +37 -58
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +32 -63
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +30 -29
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +41 -59
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +51 -98
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +70 -85
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +51 -84
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +112 -0
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +3 -3
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +35 -10
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +1 -1
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +77 -39
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +118 -35
- _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +48 -3
- _mcp_mesh/pipeline/mcp_startup/server_discovery.py +77 -48
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +2 -2
- _mcp_mesh/shared/health_check_cache.py +246 -0
- _mcp_mesh/shared/registry_client_wrapper.py +29 -2
- {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.1.dist-info}/METADATA +1 -1
- {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.1.dist-info}/RECORD +48 -37
- mesh/__init__.py +12 -2
- mesh/decorators.py +105 -39
- mesh/helpers.py +259 -0
- mesh/types.py +53 -4
- {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.1.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -117,11 +117,13 @@ class DecoratorRegistry:
|
|
|
117
117
|
if func_name in cls._mesh_tools:
|
|
118
118
|
old_func = cls._mesh_tools[func_name].function
|
|
119
119
|
cls._mesh_tools[func_name].function = new_func
|
|
120
|
-
|
|
120
|
+
logger.debug(
|
|
121
121
|
f"🔄 DecoratorRegistry: Updated '{func_name}' from {hex(id(old_func))} to {hex(id(new_func))}"
|
|
122
122
|
)
|
|
123
123
|
else:
|
|
124
|
-
|
|
124
|
+
logger.debug(
|
|
125
|
+
f"⚠️ DecoratorRegistry: Function '{func_name}' not found for update"
|
|
126
|
+
)
|
|
125
127
|
|
|
126
128
|
@classmethod
|
|
127
129
|
def register_mesh_resource(cls, func: Callable, metadata: dict[str, Any]) -> None:
|
|
@@ -623,6 +625,28 @@ class DecoratorRegistry:
|
|
|
623
625
|
cls._immediate_uvicorn_server = None
|
|
624
626
|
logger.debug("🔄 REGISTRY: Cleared immediate uvicorn server reference")
|
|
625
627
|
|
|
628
|
+
# Health check result storage
|
|
629
|
+
_health_check_result: dict | None = None
|
|
630
|
+
|
|
631
|
+
@classmethod
|
|
632
|
+
def store_health_check_result(cls, result: dict) -> None:
|
|
633
|
+
"""Store health check result for /health endpoint."""
|
|
634
|
+
cls._health_check_result = result
|
|
635
|
+
logger.debug(
|
|
636
|
+
f"💾 REGISTRY: Stored health check result: {result.get('status', 'unknown')}"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
@classmethod
|
|
640
|
+
def get_health_check_result(cls) -> dict | None:
|
|
641
|
+
"""Get stored health check result."""
|
|
642
|
+
return cls._health_check_result
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
def clear_health_check_result(cls) -> None:
|
|
646
|
+
"""Clear stored health check result."""
|
|
647
|
+
cls._health_check_result = None
|
|
648
|
+
logger.debug("🗑️ REGISTRY: Cleared health check result")
|
|
649
|
+
|
|
626
650
|
@classmethod
|
|
627
651
|
def store_fastmcp_lifespan(cls, lifespan: Any) -> None:
|
|
628
652
|
"""
|
|
@@ -132,7 +132,7 @@ class DependencyInjector:
|
|
|
132
132
|
instance: Proxy instance to register
|
|
133
133
|
"""
|
|
134
134
|
async with self._lock:
|
|
135
|
-
logger.
|
|
135
|
+
logger.debug(f"📦 Registering dependency: {name}")
|
|
136
136
|
self._dependencies[name] = instance
|
|
137
137
|
|
|
138
138
|
# Notify all functions that depend on this (using composite keys)
|
|
@@ -302,6 +302,19 @@ class DependencyInjector:
|
|
|
302
302
|
)
|
|
303
303
|
self._llm_injector.process_llm_tools(llm_tools)
|
|
304
304
|
|
|
305
|
+
def process_llm_providers(self, llm_providers: dict[str, dict[str, Any]]) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Process llm_providers from registry response and delegate to MeshLlmAgentInjector (v0.6.1).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
llm_providers: Dict mapping function_name -> ResolvedLLMProvider
|
|
311
|
+
Format: {"function_name": {"agent_id": "...", "endpoint": "...", ...}}
|
|
312
|
+
"""
|
|
313
|
+
logger.info(
|
|
314
|
+
f"🔌 DependencyInjector processing llm_providers for {len(llm_providers)} functions"
|
|
315
|
+
)
|
|
316
|
+
self._llm_injector.process_llm_providers(llm_providers)
|
|
317
|
+
|
|
305
318
|
def update_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
|
|
306
319
|
"""
|
|
307
320
|
Update llm_tools when topology changes (heartbeat updates).
|
_mcp_mesh/engine/llm_config.py
CHANGED
|
@@ -5,7 +5,7 @@ Consolidates LLM-related configuration into a single type-safe structure.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import Any, Dict, Optional, Union
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
@@ -14,16 +14,18 @@ class LLMConfig:
|
|
|
14
14
|
Configuration for MeshLlmAgent.
|
|
15
15
|
|
|
16
16
|
Consolidates provider, model, and runtime settings into a single type-safe structure.
|
|
17
|
+
Supports both direct LiteLLM providers (string) and mesh delegation (dict).
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
provider: str = "claude"
|
|
20
|
-
"""LLM provider (e.g., 'claude', 'openai'
|
|
20
|
+
provider: Union[str, Dict[str, Any]] = "claude"
|
|
21
|
+
"""LLM provider - string for direct LiteLLM (e.g., 'claude', 'openai') or dict for mesh delegation
|
|
22
|
+
Mesh delegation format: {"capability": "llm", "tags": ["claude"], "version": ">=1.0.0"}"""
|
|
21
23
|
|
|
22
24
|
model: str = "claude-3-5-sonnet-20241022"
|
|
23
|
-
"""Model name for the provider"""
|
|
25
|
+
"""Model name for the provider (only used with string provider for direct LiteLLM)"""
|
|
24
26
|
|
|
25
27
|
api_key: str = ""
|
|
26
|
-
"""API key for the provider (uses environment variable if empty)"""
|
|
28
|
+
"""API key for the provider (uses environment variable if empty, only used with string provider)"""
|
|
27
29
|
|
|
28
30
|
max_iterations: int = 10
|
|
29
31
|
"""Maximum iterations for the agentic loop"""
|
|
@@ -37,5 +39,7 @@ class LLMConfig:
|
|
|
37
39
|
raise ValueError("max_iterations must be >= 1")
|
|
38
40
|
if not self.provider:
|
|
39
41
|
raise ValueError("provider cannot be empty")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
# Only validate model for string providers (not needed for mesh delegation)
|
|
44
|
+
if isinstance(self.provider, str) and not self.model:
|
|
45
|
+
raise ValueError("model cannot be empty when using string provider")
|
|
@@ -8,7 +8,7 @@ import asyncio
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Optional
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
@@ -19,6 +19,7 @@ from .llm_errors import (
|
|
|
19
19
|
ResponseParseError,
|
|
20
20
|
ToolExecutionError,
|
|
21
21
|
)
|
|
22
|
+
from .provider_handlers import ProviderHandlerRegistry
|
|
22
23
|
from .response_parser import ResponseParser
|
|
23
24
|
from .tool_executor import ToolExecutor
|
|
24
25
|
from .tool_schema_builder import ToolSchemaBuilder
|
|
@@ -61,6 +62,8 @@ class MeshLlmAgent:
|
|
|
61
62
|
tool_proxies: Optional[dict[str, Any]] = None,
|
|
62
63
|
template_path: Optional[str] = None,
|
|
63
64
|
context_value: Optional[Any] = None,
|
|
65
|
+
provider_proxy: Optional[Any] = None,
|
|
66
|
+
vendor: Optional[str] = None,
|
|
64
67
|
):
|
|
65
68
|
"""
|
|
66
69
|
Initialize MeshLlmAgent proxy.
|
|
@@ -72,6 +75,8 @@ class MeshLlmAgent:
|
|
|
72
75
|
tool_proxies: Optional map of function_name -> proxy for tool execution
|
|
73
76
|
template_path: Optional path to Jinja2 template file for system prompt
|
|
74
77
|
context_value: Optional context for template rendering (MeshContextModel, dict, or None)
|
|
78
|
+
provider_proxy: Optional pre-resolved provider proxy for mesh delegation
|
|
79
|
+
vendor: Optional vendor name for handler selection (e.g., "anthropic", "openai")
|
|
75
80
|
"""
|
|
76
81
|
self.config = config
|
|
77
82
|
self.provider = config.provider
|
|
@@ -84,6 +89,10 @@ class MeshLlmAgent:
|
|
|
84
89
|
self.system_prompt = config.system_prompt # Public attribute for tests
|
|
85
90
|
self._iteration_count = 0
|
|
86
91
|
|
|
92
|
+
# Detect if using mesh delegation (provider is dict)
|
|
93
|
+
self._is_mesh_delegated = isinstance(self.provider, dict)
|
|
94
|
+
self._mesh_provider_proxy = provider_proxy # Pre-resolved by heartbeat
|
|
95
|
+
|
|
87
96
|
# Template rendering support (Phase 3)
|
|
88
97
|
self._template_path = template_path
|
|
89
98
|
self._context_value = context_value
|
|
@@ -96,7 +105,15 @@ class MeshLlmAgent:
|
|
|
96
105
|
# Build tool schemas for LLM (OpenAI format used by LiteLLM)
|
|
97
106
|
self._tool_schemas = ToolSchemaBuilder.build_schemas(self.tools_metadata)
|
|
98
107
|
|
|
99
|
-
#
|
|
108
|
+
# Phase 2: Get provider-specific handler
|
|
109
|
+
# This enables vendor-optimized behavior (e.g., OpenAI response_format)
|
|
110
|
+
self._provider_handler = ProviderHandlerRegistry.get_handler(vendor)
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"🎯 Using provider handler: {self._provider_handler} for vendor: {vendor}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# DEPRECATED: Legacy cached instructions (now handled by provider handlers)
|
|
116
|
+
# Kept for backward compatibility with tests
|
|
100
117
|
self._cached_tool_instructions = """
|
|
101
118
|
|
|
102
119
|
IMPORTANT TOOL CALLING RULES:
|
|
@@ -109,8 +126,6 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
109
126
|
- Once you have gathered all necessary information, provide your final response
|
|
110
127
|
"""
|
|
111
128
|
|
|
112
|
-
# Cache JSON schema instructions (output_type never changes after init)
|
|
113
|
-
# This avoids regenerating the schema on every __call__
|
|
114
129
|
schema = self.output_type.model_json_schema()
|
|
115
130
|
schema_str = json.dumps(schema, indent=2)
|
|
116
131
|
self._cached_json_instructions = (
|
|
@@ -120,7 +135,7 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
120
135
|
|
|
121
136
|
logger.debug(
|
|
122
137
|
f"🤖 MeshLlmAgent initialized: provider={config.provider}, model={config.model}, "
|
|
123
|
-
f"tools={len(filtered_tools)}, max_iterations={config.max_iterations}"
|
|
138
|
+
f"tools={len(filtered_tools)}, max_iterations={config.max_iterations}, handler={self._provider_handler}"
|
|
124
139
|
)
|
|
125
140
|
|
|
126
141
|
def set_system_prompt(self, prompt: str) -> None:
|
|
@@ -205,9 +220,7 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
205
220
|
return {}
|
|
206
221
|
|
|
207
222
|
# Check if it's a MeshContextModel (has model_dump method)
|
|
208
|
-
if hasattr(context_value, "model_dump") and callable(
|
|
209
|
-
context_value.model_dump
|
|
210
|
-
):
|
|
223
|
+
if hasattr(context_value, "model_dump") and callable(context_value.model_dump):
|
|
211
224
|
return context_value.model_dump()
|
|
212
225
|
|
|
213
226
|
# Check if it's a dict
|
|
@@ -254,12 +267,153 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
254
267
|
# Otherwise, use literal system prompt from config
|
|
255
268
|
return self.system_prompt or ""
|
|
256
269
|
|
|
257
|
-
async def
|
|
270
|
+
async def _get_mesh_provider(self) -> Any:
|
|
271
|
+
"""
|
|
272
|
+
Get the mesh provider proxy (already resolved during heartbeat).
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
UnifiedMCPProxy for the mesh provider agent
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
RuntimeError: If provider proxy not resolved
|
|
279
|
+
"""
|
|
280
|
+
if self._mesh_provider_proxy is None:
|
|
281
|
+
raise RuntimeError(
|
|
282
|
+
f"Mesh provider not resolved. Provider filter: {self.provider}. "
|
|
283
|
+
f"The provider should have been resolved during heartbeat. "
|
|
284
|
+
f"Check that a matching provider is registered in the mesh."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return self._mesh_provider_proxy
|
|
288
|
+
|
|
289
|
+
async def _call_mesh_provider(
|
|
290
|
+
self, messages: list, tools: list | None = None, **kwargs
|
|
291
|
+
) -> Any:
|
|
292
|
+
"""
|
|
293
|
+
Call mesh-delegated LLM provider agent.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
messages: List of message dicts
|
|
297
|
+
tools: Optional list of tool schemas
|
|
298
|
+
**kwargs: Additional model parameters
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
LiteLLM-compatible response object
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
RuntimeError: If provider proxy not available or invocation fails
|
|
305
|
+
"""
|
|
306
|
+
# Get the pre-resolved provider proxy
|
|
307
|
+
provider_proxy = await self._get_mesh_provider()
|
|
308
|
+
|
|
309
|
+
# Import MeshLlmRequest type
|
|
310
|
+
from mesh.types import MeshLlmRequest
|
|
311
|
+
|
|
312
|
+
# Build MeshLlmRequest
|
|
313
|
+
request = MeshLlmRequest(
|
|
314
|
+
messages=messages, tools=tools, model_params=kwargs if kwargs else None
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
logger.debug(
|
|
318
|
+
f"📤 Delegating to mesh provider: {len(messages)} messages, {len(tools) if tools else 0} tools"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Call provider's process_chat tool
|
|
322
|
+
try:
|
|
323
|
+
# provider_proxy is UnifiedMCPProxy, call it with request dict
|
|
324
|
+
# Convert dataclass to dict for MCP call
|
|
325
|
+
request_dict = {
|
|
326
|
+
"messages": request.messages,
|
|
327
|
+
"tools": request.tools,
|
|
328
|
+
"model_params": request.model_params,
|
|
329
|
+
"context": request.context,
|
|
330
|
+
"request_id": request.request_id,
|
|
331
|
+
"caller_agent": request.caller_agent,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
result = await provider_proxy(request=request_dict)
|
|
335
|
+
|
|
336
|
+
# Result is a message dict with content, role, and optionally tool_calls
|
|
337
|
+
# Parse it to create LiteLLM-compatible response
|
|
338
|
+
message_dict = result
|
|
339
|
+
|
|
340
|
+
# Create mock LiteLLM response structure
|
|
341
|
+
# This mimics litellm.completion() response format
|
|
342
|
+
class MockToolCall:
|
|
343
|
+
"""Mock tool call object matching LiteLLM structure."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, tc_dict):
|
|
346
|
+
self.id = tc_dict["id"]
|
|
347
|
+
self.type = tc_dict["type"]
|
|
348
|
+
# Create function object
|
|
349
|
+
self.function = type(
|
|
350
|
+
"Function",
|
|
351
|
+
(),
|
|
352
|
+
{
|
|
353
|
+
"name": tc_dict["function"]["name"],
|
|
354
|
+
"arguments": tc_dict["function"]["arguments"],
|
|
355
|
+
},
|
|
356
|
+
)()
|
|
357
|
+
|
|
358
|
+
class MockMessage:
|
|
359
|
+
def __init__(self, message_dict):
|
|
360
|
+
self.content = message_dict.get("content")
|
|
361
|
+
self.role = message_dict.get("role", "assistant")
|
|
362
|
+
# Extract tool_calls if present (critical for agentic loop!)
|
|
363
|
+
self.tool_calls = None
|
|
364
|
+
if "tool_calls" in message_dict and message_dict["tool_calls"]:
|
|
365
|
+
self.tool_calls = [
|
|
366
|
+
MockToolCall(tc) for tc in message_dict["tool_calls"]
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
def model_dump(self):
|
|
370
|
+
dump = {"role": self.role, "content": self.content}
|
|
371
|
+
if self.tool_calls:
|
|
372
|
+
dump["tool_calls"] = [
|
|
373
|
+
{
|
|
374
|
+
"id": tc.id,
|
|
375
|
+
"type": tc.type,
|
|
376
|
+
"function": {
|
|
377
|
+
"name": tc.function.name,
|
|
378
|
+
"arguments": tc.function.arguments,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
for tc in self.tool_calls
|
|
382
|
+
]
|
|
383
|
+
return dump
|
|
384
|
+
|
|
385
|
+
class MockChoice:
|
|
386
|
+
def __init__(self, message):
|
|
387
|
+
self.message = message
|
|
388
|
+
self.finish_reason = "stop"
|
|
389
|
+
|
|
390
|
+
class MockResponse:
|
|
391
|
+
def __init__(self, message_dict):
|
|
392
|
+
self.choices = [MockChoice(MockMessage(message_dict))]
|
|
393
|
+
|
|
394
|
+
logger.debug(
|
|
395
|
+
f"📥 Received response from mesh provider: "
|
|
396
|
+
f"content={message_dict.get('content', '')[:200]}..., "
|
|
397
|
+
f"tool_calls={len(message_dict.get('tool_calls', []))}"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return MockResponse(message_dict)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"❌ Mesh provider call failed: {e}")
|
|
404
|
+
raise RuntimeError(f"Mesh LLM provider invocation failed: {e}") from e
|
|
405
|
+
|
|
406
|
+
async def __call__(
|
|
407
|
+
self, message: Union[str, list[dict[str, Any]]], **kwargs
|
|
408
|
+
) -> Any:
|
|
258
409
|
"""
|
|
259
410
|
Execute automatic agentic loop and return typed response.
|
|
260
411
|
|
|
261
412
|
Args:
|
|
262
|
-
message:
|
|
413
|
+
message: Either:
|
|
414
|
+
- str: Single user message (will be wrapped in messages array)
|
|
415
|
+
- List[Dict[str, Any]]: Full conversation history with messages
|
|
416
|
+
in format [{"role": "user|assistant|system", "content": "..."}]
|
|
263
417
|
**kwargs: Additional arguments passed to LLM
|
|
264
418
|
|
|
265
419
|
Returns:
|
|
@@ -278,29 +432,46 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
278
432
|
"litellm is required for MeshLlmAgent. Install with: pip install litellm"
|
|
279
433
|
)
|
|
280
434
|
|
|
281
|
-
#
|
|
282
|
-
messages = []
|
|
283
|
-
|
|
284
|
-
# Render system prompt (from template or literal)
|
|
435
|
+
# Render base system prompt (from template or literal)
|
|
285
436
|
base_system_prompt = self._render_system_prompt()
|
|
286
437
|
|
|
287
|
-
#
|
|
288
|
-
|
|
438
|
+
# Phase 2: Use provider handler to format system prompt
|
|
439
|
+
# This allows vendor-specific optimizations (e.g., OpenAI skips JSON instructions)
|
|
440
|
+
system_content = self._provider_handler.format_system_prompt(
|
|
441
|
+
base_prompt=base_system_prompt,
|
|
442
|
+
tool_schemas=self._tool_schemas,
|
|
443
|
+
output_type=self.output_type,
|
|
444
|
+
)
|
|
289
445
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
446
|
+
# Debug: Log system prompt (truncated for privacy)
|
|
447
|
+
logger.debug(
|
|
448
|
+
f"📝 System prompt (formatted by {self._provider_handler}): {system_content[:200]}..."
|
|
449
|
+
)
|
|
293
450
|
|
|
294
|
-
#
|
|
295
|
-
|
|
451
|
+
# Build messages array based on input type
|
|
452
|
+
if isinstance(message, list):
|
|
453
|
+
# Multi-turn conversation - use provided messages array
|
|
454
|
+
messages = message.copy()
|
|
296
455
|
|
|
297
|
-
|
|
298
|
-
|
|
456
|
+
# Ensure system prompt is prepended if not already present
|
|
457
|
+
if not messages or messages[0].get("role") != "system":
|
|
458
|
+
messages.insert(0, {"role": "system", "content": system_content})
|
|
459
|
+
else:
|
|
460
|
+
# Replace existing system message with our constructed one
|
|
461
|
+
messages[0] = {"role": "system", "content": system_content}
|
|
299
462
|
|
|
300
|
-
|
|
301
|
-
|
|
463
|
+
# Log conversation history
|
|
464
|
+
logger.info(
|
|
465
|
+
f"🚀 Starting agentic loop with {len(messages)} messages in history"
|
|
466
|
+
)
|
|
467
|
+
else:
|
|
468
|
+
# Single-turn - build messages array from string
|
|
469
|
+
messages = [
|
|
470
|
+
{"role": "system", "content": system_content},
|
|
471
|
+
{"role": "user", "content": message},
|
|
472
|
+
]
|
|
302
473
|
|
|
303
|
-
|
|
474
|
+
logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
|
|
304
475
|
|
|
305
476
|
# Agentic loop
|
|
306
477
|
while self._iteration_count < self.max_iterations:
|
|
@@ -310,21 +481,61 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
310
481
|
)
|
|
311
482
|
|
|
312
483
|
try:
|
|
313
|
-
# Call LLM
|
|
484
|
+
# Call LLM (either direct LiteLLM or mesh-delegated)
|
|
314
485
|
try:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
486
|
+
if self._is_mesh_delegated:
|
|
487
|
+
# Mesh delegation: use provider handler to prepare vendor-specific request
|
|
488
|
+
# Phase 2: Handler prepares params including response_format for OpenAI, etc.
|
|
489
|
+
request_params = self._provider_handler.prepare_request(
|
|
490
|
+
messages=messages,
|
|
491
|
+
tools=self._tool_schemas if self._tool_schemas else None,
|
|
492
|
+
output_type=self.output_type,
|
|
493
|
+
**kwargs,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Extract model_params to send to provider
|
|
497
|
+
# Don't send messages/tools (already separate params) or model/api_key (provider has them)
|
|
498
|
+
model_params = {
|
|
499
|
+
k: v
|
|
500
|
+
for k, v in request_params.items()
|
|
501
|
+
if k not in ["messages", "tools", "model", "api_key"]
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
logger.debug(
|
|
505
|
+
f"📤 Delegating to mesh provider with handler-prepared params: "
|
|
506
|
+
f"keys={list(model_params.keys())}"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
response = await self._call_mesh_provider(
|
|
510
|
+
messages=messages,
|
|
511
|
+
tools=self._tool_schemas if self._tool_schemas else None,
|
|
512
|
+
**model_params, # Now includes response_format!
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
# Direct LiteLLM call
|
|
516
|
+
# Phase 2: Use provider handler to prepare vendor-specific request
|
|
517
|
+
request_params = self._provider_handler.prepare_request(
|
|
518
|
+
messages=messages,
|
|
519
|
+
tools=self._tool_schemas if self._tool_schemas else None,
|
|
520
|
+
output_type=self.output_type,
|
|
521
|
+
**kwargs,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Add model and API key (common to all vendors)
|
|
525
|
+
request_params["model"] = self.model
|
|
526
|
+
request_params["api_key"] = self.api_key
|
|
527
|
+
|
|
528
|
+
logger.debug(
|
|
529
|
+
f"📤 Calling LLM with handler-prepared params: "
|
|
530
|
+
f"keys={list(request_params.keys())}"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
response = await asyncio.to_thread(completion, **request_params)
|
|
323
534
|
except Exception as e:
|
|
324
535
|
# Any exception from completion call is an LLM API error
|
|
325
536
|
logger.error(f"❌ LLM API error: {e}")
|
|
326
537
|
raise LLMAPIError(
|
|
327
|
-
provider=self.provider,
|
|
538
|
+
provider=str(self.provider),
|
|
328
539
|
model=self.model,
|
|
329
540
|
original_error=e,
|
|
330
541
|
) from e
|
|
@@ -359,31 +570,6 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
359
570
|
f"📥 Raw LLM response: {assistant_message.content[:500]}..."
|
|
360
571
|
)
|
|
361
572
|
|
|
362
|
-
# REMOVE_LATER: Debug full LLM response
|
|
363
|
-
logger.warning(
|
|
364
|
-
f"🔍 REMOVE_LATER: assistant_message type: {type(assistant_message)}"
|
|
365
|
-
)
|
|
366
|
-
logger.warning(
|
|
367
|
-
f"🔍 REMOVE_LATER: assistant_message.content type: {type(assistant_message.content)}"
|
|
368
|
-
)
|
|
369
|
-
logger.warning(
|
|
370
|
-
f"🔍 REMOVE_LATER: assistant_message.content is None: {assistant_message.content is None}"
|
|
371
|
-
)
|
|
372
|
-
if assistant_message.content:
|
|
373
|
-
logger.warning(
|
|
374
|
-
f"🔍 REMOVE_LATER: Full LLM response length: {len(assistant_message.content)}"
|
|
375
|
-
)
|
|
376
|
-
logger.warning(
|
|
377
|
-
f"🔍 REMOVE_LATER: Full LLM response: {assistant_message.content!r}"
|
|
378
|
-
)
|
|
379
|
-
else:
|
|
380
|
-
logger.warning(
|
|
381
|
-
"🔍 REMOVE_LATER: assistant_message.content is empty or None!"
|
|
382
|
-
)
|
|
383
|
-
logger.warning(
|
|
384
|
-
f"🔍 REMOVE_LATER: Full assistant_message: {assistant_message}"
|
|
385
|
-
)
|
|
386
|
-
|
|
387
573
|
return self._parse_response(assistant_message.content)
|
|
388
574
|
|
|
389
575
|
except LLMAPIError:
|