flock-core 0.4.2__py3-none-any.whl → 0.4.5__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (38) hide show
  1. flock/core/__init__.py +11 -0
  2. flock/core/flock.py +144 -42
  3. flock/core/flock_agent.py +117 -4
  4. flock/core/flock_evaluator.py +1 -1
  5. flock/core/flock_factory.py +290 -2
  6. flock/core/flock_module.py +101 -0
  7. flock/core/flock_registry.py +39 -2
  8. flock/core/flock_server_manager.py +136 -0
  9. flock/core/logging/telemetry.py +1 -1
  10. flock/core/mcp/__init__.py +1 -0
  11. flock/core/mcp/flock_mcp_server.py +614 -0
  12. flock/core/mcp/flock_mcp_tool_base.py +201 -0
  13. flock/core/mcp/mcp_client.py +658 -0
  14. flock/core/mcp/mcp_client_manager.py +201 -0
  15. flock/core/mcp/mcp_config.py +237 -0
  16. flock/core/mcp/types/__init__.py +1 -0
  17. flock/core/mcp/types/callbacks.py +86 -0
  18. flock/core/mcp/types/factories.py +111 -0
  19. flock/core/mcp/types/handlers.py +240 -0
  20. flock/core/mcp/types/types.py +157 -0
  21. flock/core/mcp/util/__init__.py +0 -0
  22. flock/core/mcp/util/helpers.py +23 -0
  23. flock/core/mixin/dspy_integration.py +45 -12
  24. flock/core/serialization/flock_serializer.py +52 -1
  25. flock/core/util/spliter.py +4 -0
  26. flock/evaluators/declarative/declarative_evaluator.py +4 -3
  27. flock/mcp/servers/sse/__init__.py +1 -0
  28. flock/mcp/servers/sse/flock_sse_server.py +139 -0
  29. flock/mcp/servers/stdio/__init__.py +1 -0
  30. flock/mcp/servers/stdio/flock_stdio_server.py +138 -0
  31. flock/mcp/servers/websockets/__init__.py +1 -0
  32. flock/mcp/servers/websockets/flock_websocket_server.py +119 -0
  33. flock/modules/performance/metrics_module.py +159 -1
  34. {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/METADATA +278 -64
  35. {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/RECORD +38 -18
  36. {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/WHEEL +0 -0
  37. {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/entry_points.txt +0 -0
  38. {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,201 @@
1
+ """Represents a MCP Tool in a format which is compatible with Flock's ecosystem."""
2
+
3
+ from typing import Any, TypeVar
4
+
5
+ from dspy import Tool as DSPyTool
6
+ from mcp import Tool
7
+ from mcp.types import CallToolResult, TextContent, ToolAnnotations
8
+ from opentelemetry import trace
9
+ from pydantic import BaseModel, Field
10
+
11
+ from flock.core.logging.logging import get_logger
12
+
13
+ logger = get_logger("core.mcp.tool_base")
14
+ tracer = trace.get_tracer(__name__)
15
+
16
+ T = TypeVar("T", bound="FlockMCPToolBase")
17
+
18
+ TYPE_MAPPING = {
19
+ "string": str,
20
+ "integer": int,
21
+ "number": float,
22
+ "boolean": bool,
23
+ "array": list,
24
+ "object": dict,
25
+ }
26
+
27
+
28
+ class FlockMCPToolBase(BaseModel):
29
+ """Base Class for MCP Tools for Flock."""
30
+
31
+ name: str = Field(..., description="Name of the tool")
32
+
33
+ agent_id: str = Field(
34
+ ..., description="Associated agent_id. Used for internal tracking."
35
+ )
36
+
37
+ run_id: str = Field(
38
+ ..., description="Associated run_id. Used for internal tracking."
39
+ )
40
+
41
+ description: str | None = Field(
42
+ ..., description="A human-readable description of the tool"
43
+ )
44
+
45
+ input_schema: dict[str, Any] = Field(
46
+ ...,
47
+ description="A JSON Schema object defining the expected parameters for the tool.",
48
+ )
49
+
50
+ annotations: ToolAnnotations | None = Field(
51
+ ..., description="Optional additional tool information."
52
+ )
53
+
54
+ @classmethod
55
+ def from_mcp_tool(
56
+ cls: type[T], tool: Tool, agent_id: str, run_id: str
57
+ ) -> T:
58
+ """Convert MCP Tool to Flock Tool."""
59
+ return cls(
60
+ name=tool.name,
61
+ agent_id=agent_id,
62
+ run_id=run_id,
63
+ description=tool.description,
64
+ input_schema=tool.inputSchema,
65
+ annotations=tool.annotations,
66
+ )
67
+
68
+ @classmethod
69
+ def to_mcp_tool(cls: type[T], instance: T) -> Tool | None:
70
+ """Convert a flock mcp tool into a mcp tool."""
71
+ return Tool(
72
+ name=instance.name,
73
+ description=instance.description,
74
+ inputSchema=instance.input_schema,
75
+ annotations=instance.annotations,
76
+ )
77
+
78
+ def resolve_json_schema_reference(self, schema: dict) -> dict:
79
+ """Recursively resolve json model schema, expanding all references."""
80
+ if "$defs" not in schema and "definitions" not in schema:
81
+ return schema
82
+
83
+ def resolve_refs(obj: Any) -> Any:
84
+ if not isinstance(obj, dict[list, list]):
85
+ return obj
86
+ if isinstance(obj, dict) and "$ref" in obj:
87
+ # ref_path = obj["$ref"].split("/")[-1]
88
+ return {resolve_refs(v) for k, v in obj.items()}
89
+
90
+ return [resolve_refs(item) for item in obj]
91
+
92
+ resolved_schema = resolve_refs(schema)
93
+
94
+ resolved_schema.pop("$defs", None)
95
+ return resolved_schema
96
+
97
+ def _convert_input_schema_to_tool_args(
98
+ self, input_schema: dict[str, Any]
99
+ ) -> tuple[dict[str, Any], dict[str, type], dict[str, str]]:
100
+ """Convert an input schema to tool arguments compatible with Dspy Tool.
101
+
102
+ Args:
103
+ schema: an input schema describing the tool's input parameters
104
+
105
+ Returns:
106
+ A tuple of (args, arg_types, arg_desc) for Dspy Tool definition
107
+ """
108
+ args, arg_types, arg_desc = {}, {}, {}
109
+ properties = input_schema.get("properties")
110
+ if properties is None:
111
+ return args, arg_types, arg_desc
112
+
113
+ required = input_schema.get("required", [])
114
+
115
+ defs = input_schema.get("$defs", {})
116
+
117
+ for name, prop in properties.items():
118
+ if len(defs) > 0:
119
+ prop = self.resolve_json_schema_reference(
120
+ {"$defs": defs, **prop}
121
+ )
122
+
123
+ args[name] = prop
124
+
125
+ arg_types[name] = TYPE_MAPPING.get(prop.get("type"), Any)
126
+ arg_desc[name] = prop.get("description", "No description provided")
127
+ if name in required:
128
+ arg_desc[name] += " (Required)"
129
+
130
+ return args, arg_types, arg_desc
131
+
132
+ def _convert_mcp_tool_result(
133
+ self, call_tool_result: CallToolResult
134
+ ) -> str | list[Any]:
135
+ text_contents: list[TextContent] = []
136
+ non_text_contents = []
137
+
138
+ for content in call_tool_result.content:
139
+ if isinstance(content, TextContent):
140
+ text_contents.append(content)
141
+ else:
142
+ non_text_contents.append(content)
143
+
144
+ tool_content = [content.text for content in text_contents]
145
+ if len(text_contents) == 1:
146
+ tool_content = tool_content[0]
147
+
148
+ if call_tool_result.isError:
149
+ logger.error(f"MCP Tool '{self.name}' returned an error.")
150
+
151
+ return tool_content or non_text_contents
152
+
153
+ def on_error(self, res: CallToolResult, **kwargs) -> None:
154
+ """Optional on error hook."""
155
+ # leave it for now, might be useful for more sophisticated processing.
156
+ logger.error(f"Tool: '{self.name}' on_error: Tool returned error.")
157
+ return res
158
+
159
+ def as_dspy_tool(self, server: Any) -> DSPyTool:
160
+ """Wrap this tool as a DSPyTool for downstream."""
161
+ args, arg_type, args_desc = self._convert_input_schema_to_tool_args(
162
+ self.input_schema
163
+ )
164
+
165
+ async def func(*args, **kwargs):
166
+ with tracer.start_as_current_span(f"tool.{self.name}.call") as span:
167
+ span.set_attribute("tool.name", self.name)
168
+ try:
169
+ logger.debug(f"Tool: {self.name}: getting client.")
170
+
171
+ server_name = server.config.name
172
+ logger.debug(
173
+ f"Tool: {self.name}: got client for server '{server_name}' for agent {self.agent_id} on run {self.run_id}"
174
+ )
175
+ logger.debug(
176
+ f"Tool: {self.name}: calling server '{server_name}'"
177
+ )
178
+ result = await server.call_tool(
179
+ agent_id=self.agent_id,
180
+ run_id=self.run_id,
181
+ name=self.name,
182
+ arguments=kwargs,
183
+ )
184
+ logger.debug(
185
+ f"Tool: Called Tool: {self.name} on server '{server_name}'. Returning result to LLM."
186
+ )
187
+ return self._convert_mcp_tool_result(result)
188
+ except Exception as e:
189
+ logger.error(
190
+ f"Tool: Exception ocurred when calling tool '{self.name}': {e}"
191
+ )
192
+ span.record_exception(e)
193
+
194
+ return DSPyTool(
195
+ func=func,
196
+ name=self.name,
197
+ desc=self.description,
198
+ args=args,
199
+ arg_types=arg_type,
200
+ arg_desc=args_desc,
201
+ )