mcp-mesh 0.7.20__py3-none-any.whl → 0.8.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.
Files changed (124) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +13 -15
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +29 -10
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/signature_analyzer.py +58 -68
  14. _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
  15. _mcp_mesh/pipeline/__init__.py +9 -20
  16. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  17. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  18. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
  19. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  20. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  21. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  22. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  23. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  24. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  25. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
  26. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  27. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  28. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  30. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  32. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  33. _mcp_mesh/reload.py +1 -3
  34. _mcp_mesh/shared/__init__.py +2 -8
  35. _mcp_mesh/shared/config_resolver.py +124 -80
  36. _mcp_mesh/shared/defaults.py +89 -14
  37. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  38. _mcp_mesh/shared/host_resolver.py +8 -46
  39. _mcp_mesh/shared/server_discovery.py +115 -86
  40. _mcp_mesh/shared/simple_shutdown.py +44 -86
  41. _mcp_mesh/tracing/execution_tracer.py +2 -6
  42. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  43. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  44. _mcp_mesh/tracing/utils.py +29 -15
  45. _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
  46. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
  47. mcp_mesh-0.8.0.dist-info/RECORD +85 -0
  48. mesh/__init__.py +12 -1
  49. mesh/decorators.py +248 -33
  50. mesh/helpers.py +52 -0
  51. mesh/types.py +40 -13
  52. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  53. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  54. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  55. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  59. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  60. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  61. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  62. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  63. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  100. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  101. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  102. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  103. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  104. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  105. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  106. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  107. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  108. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -247
  109. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  110. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  111. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  112. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  113. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  114. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  115. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  116. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  117. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  118. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  119. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  120. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  121. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  122. mcp_mesh-0.7.20.dist-info/RECORD +0 -152
  123. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
  124. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,11 +17,16 @@ Features:
17
17
 
18
18
  import json
19
19
  import logging
20
- from typing import Any, Optional, get_args, get_origin
20
+ from typing import Any, Optional
21
21
 
22
22
  from pydantic import BaseModel
23
23
 
24
- from .base_provider_handler import BaseProviderHandler
24
+ from .base_provider_handler import (
25
+ BASE_TOOL_INSTRUCTIONS,
26
+ BaseProviderHandler,
27
+ CLAUDE_ANTI_XML_INSTRUCTION,
28
+ make_schema_strict,
29
+ )
25
30
 
26
31
  logger = logging.getLogger(__name__)
27
32
 
@@ -141,51 +146,6 @@ class ClaudeHandler(BaseProviderHandler):
141
146
  # Default to strict for unknown types
142
147
  return OUTPUT_MODE_STRICT
143
148
 
144
- def _make_schema_strict(self, schema: dict[str, Any]) -> dict[str, Any]:
145
- """
146
- Make a JSON schema strict for Claude's structured output.
147
-
148
- Claude requires additionalProperties: false on all object types.
149
- This recursively processes the schema to add this constraint.
150
-
151
- Args:
152
- schema: JSON schema dict
153
-
154
- Returns:
155
- Schema with additionalProperties: false on all objects
156
- """
157
- if not isinstance(schema, dict):
158
- return schema
159
-
160
- result = schema.copy()
161
-
162
- # If this is an object type, add additionalProperties: false
163
- if result.get("type") == "object":
164
- result["additionalProperties"] = False
165
-
166
- # Recursively process nested schemas
167
- if "properties" in result:
168
- result["properties"] = {
169
- k: self._make_schema_strict(v) for k, v in result["properties"].items()
170
- }
171
-
172
- # Process $defs (Pydantic uses this for nested models)
173
- if "$defs" in result:
174
- result["$defs"] = {
175
- k: self._make_schema_strict(v) for k, v in result["$defs"].items()
176
- }
177
-
178
- # Process items for arrays
179
- if "items" in result:
180
- result["items"] = self._make_schema_strict(result["items"])
181
-
182
- # Process anyOf, oneOf, allOf
183
- for key in ["anyOf", "oneOf", "allOf"]:
184
- if key in result:
185
- result[key] = [self._make_schema_strict(s) for s in result[key]]
186
-
187
- return result
188
-
189
149
  def _apply_prompt_caching(
190
150
  self, messages: list[dict[str, Any]]
191
151
  ) -> list[dict[str, Any]]:
@@ -302,9 +262,10 @@ class ClaudeHandler(BaseProviderHandler):
302
262
  # Only add response_format in "strict" mode
303
263
  if determined_mode == OUTPUT_MODE_STRICT:
304
264
  # Claude requires additionalProperties: false on all object types
265
+ # Unlike OpenAI/Gemini, Claude doesn't require all properties in 'required'
305
266
  if isinstance(output_type, type) and issubclass(output_type, BaseModel):
306
267
  schema = output_type.model_json_schema()
307
- strict_schema = self._make_schema_strict(schema)
268
+ strict_schema = make_schema_strict(schema, add_all_required=False)
308
269
  request_params["response_format"] = {
309
270
  "type": "json_schema",
310
271
  "json_schema": {
@@ -346,15 +307,12 @@ class ClaudeHandler(BaseProviderHandler):
346
307
  # Add tool calling instructions if tools available
347
308
  # These prevent Claude from using XML-style <invoke> syntax
348
309
  if tool_schemas:
349
- system_content += """
350
-
351
- IMPORTANT TOOL CALLING RULES:
352
- - You have access to tools that you can call to gather information
353
- - Make ONE tool call at a time
354
- - NEVER use XML-style syntax like <invoke name="tool_name"/>
355
- - After receiving tool results, you can make additional calls if needed
356
- - Once you have all needed information, provide your final response
357
- """
310
+ # Use base instructions but insert anti-XML rule for Claude
311
+ instructions = BASE_TOOL_INSTRUCTIONS.replace(
312
+ "- Make ONE tool call at a time",
313
+ f"- Make ONE tool call at a time\n{CLAUDE_ANTI_XML_INSTRUCTION}",
314
+ )
315
+ system_content += instructions
358
316
 
359
317
  # Add output format instructions based on mode
360
318
  if determined_mode == OUTPUT_MODE_TEXT:
@@ -0,0 +1,181 @@
1
+ """
2
+ Gemini/Google provider handler.
3
+
4
+ Optimized for Gemini models (Gemini 2.0 Flash, Gemini 1.5 Pro, etc.)
5
+ using Google's best practices for tool calling and structured output.
6
+
7
+ Features:
8
+ - Native structured output via response_format (similar to OpenAI)
9
+ - Native function calling support
10
+ - Support for Gemini 2.x and 3.x models
11
+ - Large context windows (up to 2M tokens)
12
+
13
+ Reference:
14
+ - https://docs.litellm.ai/docs/providers/gemini
15
+ - https://ai.google.dev/gemini-api/docs
16
+ """
17
+
18
+ import logging
19
+ from typing import Any, Optional
20
+
21
+ from pydantic import BaseModel
22
+
23
+ from .base_provider_handler import (
24
+ BASE_TOOL_INSTRUCTIONS,
25
+ BaseProviderHandler,
26
+ make_schema_strict,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class GeminiHandler(BaseProviderHandler):
33
+ """
34
+ Provider handler for Google Gemini models.
35
+
36
+ Gemini Characteristics:
37
+ - Native structured output via response_format parameter (LiteLLM translates)
38
+ - Native function calling support
39
+ - Large context windows (1M-2M tokens)
40
+ - Multimodal support (text, images, video, audio)
41
+ - Works well with concise, focused prompts
42
+
43
+ Key Similarities with OpenAI:
44
+ - Uses response_format for structured output (via LiteLLM translation)
45
+ - Native function calling format
46
+ - Similar schema enforcement requirements
47
+
48
+ Supported Models (via LiteLLM):
49
+ - gemini/gemini-2.0-flash (fast, efficient)
50
+ - gemini/gemini-2.0-flash-lite (fastest, most efficient)
51
+ - gemini/gemini-1.5-pro (high capability)
52
+ - gemini/gemini-1.5-flash (balanced)
53
+ - gemini/gemini-3-flash-preview (reasoning support)
54
+ - gemini/gemini-3-pro-preview (advanced reasoning)
55
+
56
+ Reference:
57
+ https://docs.litellm.ai/docs/providers/gemini
58
+ """
59
+
60
+ def __init__(self):
61
+ """Initialize Gemini handler."""
62
+ super().__init__(vendor="gemini")
63
+
64
+ def prepare_request(
65
+ self,
66
+ messages: list[dict[str, Any]],
67
+ tools: Optional[list[dict[str, Any]]],
68
+ output_type: type,
69
+ **kwargs: Any,
70
+ ) -> dict[str, Any]:
71
+ """
72
+ Prepare request parameters for Gemini API via LiteLLM.
73
+
74
+ Gemini Strategy:
75
+ - Use response_format parameter for structured JSON output
76
+ - LiteLLM handles translation to Gemini's native format
77
+ - Skip structured output for str return types (text mode)
78
+
79
+ Args:
80
+ messages: List of message dicts
81
+ tools: Optional list of tool schemas
82
+ output_type: Return type (str or Pydantic model)
83
+ **kwargs: Additional model parameters
84
+
85
+ Returns:
86
+ Dictionary of parameters for litellm.completion()
87
+ """
88
+ # Build base request
89
+ request_params = {
90
+ "messages": messages,
91
+ **kwargs, # Pass through temperature, max_tokens, etc.
92
+ }
93
+
94
+ # Add tools if provided
95
+ # LiteLLM will convert OpenAI tool format to Gemini's function_declarations
96
+ if tools:
97
+ request_params["tools"] = tools
98
+
99
+ # Skip structured output for str return type (text mode)
100
+ if output_type is str:
101
+ return request_params
102
+
103
+ # Only add response_format for Pydantic models
104
+ if not (isinstance(output_type, type) and issubclass(output_type, BaseModel)):
105
+ return request_params
106
+
107
+ # Add response_format for structured output
108
+ # LiteLLM translates this to Gemini's native format
109
+ schema = output_type.model_json_schema()
110
+
111
+ # Transform schema for strict mode compliance
112
+ # Gemini requires additionalProperties: false and all properties in required
113
+ schema = make_schema_strict(schema, add_all_required=True)
114
+
115
+ # Gemini structured output format (via LiteLLM)
116
+ request_params["response_format"] = {
117
+ "type": "json_schema",
118
+ "json_schema": {
119
+ "name": output_type.__name__,
120
+ "schema": schema,
121
+ "strict": True, # Enforce schema compliance
122
+ },
123
+ }
124
+
125
+ return request_params
126
+
127
+ def format_system_prompt(
128
+ self,
129
+ base_prompt: str,
130
+ tool_schemas: Optional[list[dict[str, Any]]],
131
+ output_type: type,
132
+ ) -> str:
133
+ """
134
+ Format system prompt for Gemini (concise approach).
135
+
136
+ Gemini Strategy:
137
+ 1. Use base prompt as-is
138
+ 2. Add tool calling instructions if tools present
139
+ 3. Minimal JSON instructions (response_format handles structure)
140
+ 4. Keep prompt concise - Gemini works well with clear, direct prompts
141
+
142
+ Args:
143
+ base_prompt: Base system prompt
144
+ tool_schemas: Optional tool schemas
145
+ output_type: Expected response type (str or Pydantic model)
146
+
147
+ Returns:
148
+ Formatted system prompt optimized for Gemini
149
+ """
150
+ system_content = base_prompt
151
+
152
+ # Add tool calling instructions if tools available
153
+ if tool_schemas:
154
+ system_content += BASE_TOOL_INSTRUCTIONS
155
+
156
+ # Skip JSON note for str return type (text mode)
157
+ if output_type is str:
158
+ return system_content
159
+
160
+ # Add brief JSON note (response_format handles enforcement)
161
+ if isinstance(output_type, type) and issubclass(output_type, BaseModel):
162
+ system_content += f"\n\nYour final response will be structured as JSON matching the {output_type.__name__} format."
163
+
164
+ return system_content
165
+
166
+ def get_vendor_capabilities(self) -> dict[str, bool]:
167
+ """
168
+ Return Gemini-specific capabilities.
169
+
170
+ Returns:
171
+ Capability flags for Gemini
172
+ """
173
+ return {
174
+ "native_tool_calling": True, # Gemini has native function calling
175
+ "structured_output": True, # Supports structured output via response_format
176
+ "streaming": True, # Supports streaming
177
+ "vision": True, # Gemini supports multimodal (images, video, audio)
178
+ "json_mode": True, # Native JSON mode via response_format
179
+ "large_context": True, # Up to 2M tokens context window
180
+ }
181
+
@@ -5,12 +5,15 @@ Optimized for OpenAI models (GPT-4, GPT-4 Turbo, GPT-3.5-turbo)
5
5
  using OpenAI's native structured output capabilities.
6
6
  """
7
7
 
8
- import json
9
8
  from typing import Any, Optional
10
9
 
11
10
  from pydantic import BaseModel
12
11
 
13
- from .base_provider_handler import BaseProviderHandler
12
+ from .base_provider_handler import (
13
+ BASE_TOOL_INSTRUCTIONS,
14
+ BaseProviderHandler,
15
+ make_schema_strict,
16
+ )
14
17
 
15
18
 
16
19
  class OpenAIHandler(BaseProviderHandler):
@@ -94,8 +97,8 @@ class OpenAIHandler(BaseProviderHandler):
94
97
  schema = output_type.model_json_schema()
95
98
 
96
99
  # Transform schema for OpenAI strict mode
97
- # OpenAI requires additionalProperties: false on all object schemas
98
- schema = self._add_additional_properties_false(schema)
100
+ # OpenAI requires additionalProperties: false and all properties in required
101
+ schema = make_schema_strict(schema, add_all_required=True)
99
102
 
100
103
  # OpenAI structured output format
101
104
  # See: https://platform.openai.com/docs/guides/structured-outputs
@@ -143,14 +146,7 @@ class OpenAIHandler(BaseProviderHandler):
143
146
 
144
147
  # Add tool calling instructions if tools available
145
148
  if tool_schemas:
146
- system_content += """
147
-
148
- IMPORTANT TOOL CALLING RULES:
149
- - You have access to tools that you can call to gather information
150
- - Make ONE tool call at a time
151
- - After receiving tool results, you can make additional calls if needed
152
- - Once you have all needed information, provide your final response
153
- """
149
+ system_content += BASE_TOOL_INSTRUCTIONS
154
150
 
155
151
  # Skip JSON note for str return type (text mode)
156
152
  if output_type is str:
@@ -181,54 +177,3 @@ IMPORTANT TOOL CALLING RULES:
181
177
  "json_mode": True, # Has dedicated JSON mode via response_format
182
178
  }
183
179
 
184
- def _add_additional_properties_false(
185
- self, schema: dict[str, Any]
186
- ) -> dict[str, Any]:
187
- """
188
- Recursively add additionalProperties: false to all object schemas.
189
-
190
- OpenAI strict mode requires this for all object schemas.
191
- See: https://platform.openai.com/docs/guides/structured-outputs
192
-
193
- Args:
194
- schema: JSON schema from Pydantic model
195
-
196
- Returns:
197
- Modified schema with additionalProperties: false on all objects
198
- """
199
- import copy
200
-
201
- schema = copy.deepcopy(schema)
202
- self._add_additional_properties_recursive(schema)
203
- return schema
204
-
205
- def _add_additional_properties_recursive(self, obj: Any) -> None:
206
- """Recursively process schema for OpenAI strict mode compliance."""
207
- if isinstance(obj, dict):
208
- # If this is an object type, add additionalProperties: false
209
- # and ensure required includes all properties
210
- if obj.get("type") == "object":
211
- obj["additionalProperties"] = False
212
- # OpenAI strict mode: required must include ALL property keys
213
- if "properties" in obj:
214
- obj["required"] = list(obj["properties"].keys())
215
-
216
- # Process $defs (Pydantic uses this for nested models)
217
- if "$defs" in obj:
218
- for def_schema in obj["$defs"].values():
219
- self._add_additional_properties_recursive(def_schema)
220
-
221
- # Process properties
222
- if "properties" in obj:
223
- for prop_schema in obj["properties"].values():
224
- self._add_additional_properties_recursive(prop_schema)
225
-
226
- # Process items (for arrays)
227
- if "items" in obj:
228
- self._add_additional_properties_recursive(obj["items"])
229
-
230
- # Process anyOf, oneOf, allOf
231
- for key in ("anyOf", "oneOf", "allOf"):
232
- if key in obj:
233
- for item in obj[key]:
234
- self._add_additional_properties_recursive(item)
@@ -5,10 +5,11 @@ Manages selection and instantiation of provider handlers based on vendor name.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Dict, Optional, Type
8
+ from typing import Optional
9
9
 
10
10
  from .base_provider_handler import BaseProviderHandler
11
11
  from .claude_handler import ClaudeHandler
12
+ from .gemini_handler import GeminiHandler
12
13
  from .generic_handler import GenericHandler
13
14
  from .openai_handler import OpenAIHandler
14
15
 
@@ -39,16 +40,17 @@ class ProviderHandlerRegistry:
39
40
  """
40
41
 
41
42
  # Built-in vendor mappings
42
- _handlers: Dict[str, Type[BaseProviderHandler]] = {
43
+ _handlers: dict[str, type[BaseProviderHandler]] = {
43
44
  "anthropic": ClaudeHandler,
44
45
  "openai": OpenAIHandler,
46
+ "gemini": GeminiHandler,
45
47
  }
46
48
 
47
49
  # Cache of instantiated handlers (singleton per vendor)
48
- _instances: Dict[str, BaseProviderHandler] = {}
50
+ _instances: dict[str, BaseProviderHandler] = {}
49
51
 
50
52
  @classmethod
51
- def register(cls, vendor: str, handler_class: Type[BaseProviderHandler]) -> None:
53
+ def register(cls, vendor: str, handler_class: type[BaseProviderHandler]) -> None:
52
54
  """
53
55
  Register a custom provider handler.
54
56
 
@@ -73,7 +75,9 @@ class ProviderHandlerRegistry:
73
75
  )
74
76
 
75
77
  cls._handlers[vendor] = handler_class
76
- logger.info(f"📝 Registered provider handler: {vendor} -> {handler_class.__name__}")
78
+ logger.info(
79
+ f"📝 Registered provider handler: {vendor} -> {handler_class.__name__}"
80
+ )
77
81
 
78
82
  # Clear cached instance if it exists (force re-instantiation)
79
83
  if vendor in cls._instances:
@@ -119,9 +123,7 @@ class ProviderHandlerRegistry:
119
123
  # Get handler class (or fallback to Generic)
120
124
  if vendor in cls._handlers:
121
125
  handler_class = cls._handlers[vendor]
122
- logger.info(
123
- f"✅ Selected {handler_class.__name__} for vendor: {vendor}"
124
- )
126
+ logger.info(f"✅ Selected {handler_class.__name__} for vendor: {vendor}")
125
127
  else:
126
128
  handler_class = GenericHandler
127
129
  if vendor != "unknown":
@@ -132,14 +134,18 @@ class ProviderHandlerRegistry:
132
134
  logger.debug("Using GenericHandler for unknown vendor")
133
135
 
134
136
  # Instantiate and cache
135
- handler = handler_class() if handler_class != GenericHandler else GenericHandler(vendor)
137
+ handler = (
138
+ handler_class()
139
+ if handler_class != GenericHandler
140
+ else GenericHandler(vendor)
141
+ )
136
142
  cls._instances[vendor] = handler
137
143
 
138
144
  logger.debug(f"🆕 Instantiated handler: {handler}")
139
145
  return handler
140
146
 
141
147
  @classmethod
142
- def list_vendors(cls) -> Dict[str, str]:
148
+ def list_vendors(cls) -> dict[str, str]:
143
149
  """
144
150
  List all registered vendors and their handlers.
145
151
 
@@ -94,8 +94,9 @@ class ResponseParser:
94
94
 
95
95
  Tries multiple strategies to find JSON in mixed responses:
96
96
  1. Find ```json ... ``` code fence blocks
97
- 2. Find any JSON object {...} in the content
98
- 3. Return original content if no extraction needed
97
+ 2. Find any JSON object {...} using progressive json.loads
98
+ 3. Find any JSON array [...] using progressive json.loads
99
+ 4. Return original content if no extraction needed
99
100
 
100
101
  Args:
101
102
  content: Raw content that may contain narrative, XML, and JSON
@@ -109,25 +110,70 @@ class ResponseParser:
109
110
  extracted = json_match.group(1).strip()
110
111
  return extracted
111
112
 
112
- # Strategy 2: Try to find any JSON object {...} in content
113
- # Look for balanced braces starting with { and ending with }
113
+ # Strategy 2: Try to find JSON object using progressive json.loads
114
+ # This correctly handles braces inside string values
114
115
  brace_start = content.find("{")
115
116
  if brace_start != -1:
116
- # Find matching closing brace
117
- brace_count = 0
118
- for i in range(brace_start, len(content)):
119
- if content[i] == "{":
120
- brace_count += 1
121
- elif content[i] == "}":
122
- brace_count -= 1
123
- if brace_count == 0:
124
- # Found matching brace
125
- extracted = content[brace_start : i + 1]
126
- return extracted
117
+ result = ResponseParser._try_progressive_parse(
118
+ content, brace_start, "{", "}"
119
+ )
120
+ if result:
121
+ return result
122
+
123
+ # Strategy 3: Try to find JSON array using progressive json.loads
124
+ bracket_start = content.find("[")
125
+ if bracket_start != -1:
126
+ result = ResponseParser._try_progressive_parse(
127
+ content, bracket_start, "[", "]"
128
+ )
129
+ if result:
130
+ return result
127
131
 
128
132
  # No JSON found, return original
129
133
  return content
130
134
 
135
+ @staticmethod
136
+ def _try_progressive_parse(
137
+ content: str, start: int, open_char: str, close_char: str
138
+ ) -> str | None:
139
+ """
140
+ Try to extract valid JSON by progressively extending the end position.
141
+ This correctly handles braces/brackets inside string values.
142
+
143
+ Args:
144
+ content: The full content string
145
+ start: Starting index of the JSON
146
+ open_char: Opening character ('{' or '[')
147
+ close_char: Closing character ('}' or ']')
148
+
149
+ Returns:
150
+ Extracted JSON string or None if not found
151
+ """
152
+ # Find potential end positions based on depth counting
153
+ depth = 0
154
+ potential_ends: list[int] = []
155
+
156
+ for i in range(start, len(content)):
157
+ char = content[i]
158
+ if char == open_char:
159
+ depth += 1
160
+ elif char == close_char:
161
+ depth -= 1
162
+ if depth == 0:
163
+ potential_ends.append(i)
164
+
165
+ # Try each potential end position (shortest first for efficiency)
166
+ for end in potential_ends:
167
+ candidate = content[start : end + 1]
168
+ try:
169
+ json.loads(candidate)
170
+ return candidate
171
+ except json.JSONDecodeError:
172
+ # Not valid JSON, try next potential end
173
+ continue
174
+
175
+ return None
176
+
131
177
  @staticmethod
132
178
  def _strip_markdown_fences(content: str) -> str:
133
179
  """