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.
- flock/core/__init__.py +11 -0
- flock/core/flock.py +144 -42
- flock/core/flock_agent.py +117 -4
- flock/core/flock_evaluator.py +1 -1
- flock/core/flock_factory.py +290 -2
- flock/core/flock_module.py +101 -0
- flock/core/flock_registry.py +39 -2
- flock/core/flock_server_manager.py +136 -0
- flock/core/logging/telemetry.py +1 -1
- flock/core/mcp/__init__.py +1 -0
- flock/core/mcp/flock_mcp_server.py +614 -0
- flock/core/mcp/flock_mcp_tool_base.py +201 -0
- flock/core/mcp/mcp_client.py +658 -0
- flock/core/mcp/mcp_client_manager.py +201 -0
- flock/core/mcp/mcp_config.py +237 -0
- flock/core/mcp/types/__init__.py +1 -0
- flock/core/mcp/types/callbacks.py +86 -0
- flock/core/mcp/types/factories.py +111 -0
- flock/core/mcp/types/handlers.py +240 -0
- flock/core/mcp/types/types.py +157 -0
- flock/core/mcp/util/__init__.py +0 -0
- flock/core/mcp/util/helpers.py +23 -0
- flock/core/mixin/dspy_integration.py +45 -12
- flock/core/serialization/flock_serializer.py +52 -1
- flock/core/util/spliter.py +4 -0
- flock/evaluators/declarative/declarative_evaluator.py +4 -3
- flock/mcp/servers/sse/__init__.py +1 -0
- flock/mcp/servers/sse/flock_sse_server.py +139 -0
- flock/mcp/servers/stdio/__init__.py +1 -0
- flock/mcp/servers/stdio/flock_stdio_server.py +138 -0
- flock/mcp/servers/websockets/__init__.py +1 -0
- flock/mcp/servers/websockets/flock_websocket_server.py +119 -0
- flock/modules/performance/metrics_module.py +159 -1
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/METADATA +278 -64
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/RECORD +38 -18
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/WHEEL +0 -0
- {flock_core-0.4.2.dist-info → flock_core-0.4.5.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|