darabos commited on
Commit
8d41b75
·
1 Parent(s): 768b26e

Make MCP+Agents part of lynxkite-lynxscribe.

Browse files
examples/LynxScribe/MCP/demo.py CHANGED
@@ -1,15 +1,5 @@
1
- import enum
2
  from lynxkite_core import ops
3
  from lynxkite_graph_analytics import core
4
- import fastapi
5
- from lynxscribe.core.llm.base import get_llm_engine
6
- import contextlib
7
- import mcp
8
- from mcp.client.stdio import stdio_client
9
- from lynxscribe.components.task_solver import TaskSolver
10
- from lynxscribe.core.models.prompts import Function, Tool
11
-
12
- FRONTEND_URL = "http://localhost:8501/"
13
 
14
  lsop = ops.op_registration(
15
  "LynxKite Graph Analytics", "LynxScribe", dir="bottom-to-top", color="blue"
@@ -17,47 +7,6 @@ lsop = ops.op_registration(
17
  dsop = ops.op_registration("LynxKite Graph Analytics", "Data Science")
18
 
19
 
20
- @lsop("Chat frontend", color="gray", outputs=[], view="service")
21
- def chat_frontend(agent: dict):
22
- agent = agent["agent"]
23
- return Agent(
24
- agent["name"],
25
- agent["description"],
26
- agent["system_prompt"],
27
- agent["mcp_servers"],
28
- [chat_frontend(a) for a in agent["sub_agents"]],
29
- )
30
-
31
-
32
- @lsop("Agent")
33
- def agent(
34
- tools: list[dict],
35
- *,
36
- name: str = "",
37
- description: ops.LongStr = "This agent helps with various tasks.",
38
- system_prompt: ops.LongStr = "You are a helpful assistant.",
39
- ):
40
- prompt = [system_prompt]
41
- for tool in tools:
42
- if tool.get("extra_prompt"):
43
- prompt.append(tool["extra_prompt"])
44
- return {
45
- "agent": {
46
- "name": name,
47
- "description": description,
48
- "system_prompt": "\n".join(prompt),
49
- "mcp_servers": [t["command"] for t in tools if "command" in t],
50
- "sub_agents": [t for t in tools if "agent" in t],
51
- }
52
- }
53
-
54
-
55
- @lsop("MCP: Custom", color="green")
56
- def custom_mcp(*, command: str, extra_prompt: ops.LongStr):
57
- if command.strip():
58
- return {"command": command.strip().split(), "extra_prompt": extra_prompt}
59
-
60
-
61
  @lsop("MCP: Query database with SQL", color="green")
62
  def sql_tool(db: str):
63
  return {
@@ -75,189 +24,3 @@ is out of order currently. Please use `query` instead."
75
  def db(data_pipeline: list[core.Bundle]):
76
  # The source of this file: https://github.com/biggraph/lynxscribe/pull/416
77
  return "movie_data.sqlite.db"
78
-
79
-
80
- class MCPSearchEngine(str, enum.Enum):
81
- Google = "Google"
82
- Bing = "Bing"
83
- DuckDuckGo = "DuckDuckGo"
84
-
85
-
86
- @lsop("MCP: Search web", color="green")
87
- def web_search(*, engine: MCPSearchEngine = MCPSearchEngine.Google):
88
- match engine:
89
- case MCPSearchEngine.Google:
90
- return {"command": ["npx", "-y", "https://github.com/pskill9/web-search"]}
91
- case MCPSearchEngine.Bing:
92
- return {"command": ["uvx", "bing-search-mcp"]}
93
- case MCPSearchEngine.DuckDuckGo:
94
- return {"command": ["uvx", "duckduckgo-mcp-server"]}
95
-
96
-
97
- @lsop("MCP: Financial data", color="green")
98
- def financial_data():
99
- return {"command": ["uvx", "investor-agent"]}
100
-
101
-
102
- @lsop("MCP: Calculator", color="green")
103
- def calculator():
104
- return {"command": ["uvx", "mcp-server-calculator"]}
105
-
106
-
107
- class Agent:
108
- def __init__(
109
- self,
110
- name: str,
111
- description: str,
112
- prompt: str,
113
- mcp_servers: list[list[str]],
114
- agents: list["Agent"],
115
- ):
116
- self.name = name
117
- self.description = description
118
- self.prompt = prompt
119
- self.mcp_servers = mcp_servers
120
- self.agents = agents
121
- self.mcp_client = None
122
- self.task_solver = None
123
-
124
- async def init(self):
125
- if self.task_solver is not None:
126
- return
127
- self.mcp_client = MultiMCPClient()
128
- await self.mcp_client.connect(self.mcp_servers)
129
- agents_as_functions = [agent.as_function() for agent in self.agents]
130
- self.task_solver = TaskSolver(
131
- llm=get_llm_engine(name="openai"),
132
- model="gpt-4.1-nano",
133
- initial_messages=[self.prompt],
134
- functions=[*self.mcp_client.tools, *agents_as_functions],
135
- tool_choice="required",
136
- temperature=0.0,
137
- max_tool_call_steps=999,
138
- )
139
-
140
- def get_description(self, url: str) -> str:
141
- return f"[Go to frontend]({FRONTEND_URL}?service={url}/chat/completions)"
142
-
143
- async def get(self, request: fastapi.Request) -> dict:
144
- if request.state.remaining_path == "models":
145
- return {
146
- "object": "list",
147
- "data": [
148
- {
149
- "id": "LynxScribe",
150
- "object": "model",
151
- "created": 0,
152
- "owned_by": "lynxkite",
153
- "meta": {"profile_image_url": "https://lynxkite.com/favicon.png"},
154
- }
155
- ],
156
- }
157
- return {"error": "Not found"}
158
-
159
- async def post(self, request: fastapi.Request) -> dict:
160
- if request.state.remaining_path == "chat/completions":
161
- request = await request.json()
162
- assert not request["stream"]
163
- await self.init()
164
- res = await self.task_solver.solve(request["messages"][-1]["content"])
165
- return {"choices": [{"message": {"role": "assistant", "content": res}}]}
166
-
167
- return {"error": "Not found"}
168
-
169
- def as_function(self):
170
- """A callable that can be used as a tool by another Agent."""
171
-
172
- # Find the value of x given that 4*x^4 = 44. Compute the numerical value.
173
- async def ask(message):
174
- print(f"Calling agent {self.name} with message: {message}")
175
- await self.init()
176
- res = await self.task_solver.solve(message)
177
- print(f"Agent {self.name} response: {res}")
178
- return res
179
-
180
- ask.__name__ = "ask_" + self.name.lower().replace(" ", "_")
181
- ask._tool = Tool(
182
- type="function",
183
- function=Function(
184
- name=ask.__name__,
185
- description=self.description,
186
- parameters={
187
- "type": "object",
188
- "required": ["message"],
189
- "properties": {
190
- "message": {
191
- "type": "string",
192
- "description": "The question to ask the agent.",
193
- }
194
- },
195
- },
196
- ),
197
- )
198
- return ask
199
-
200
-
201
- class MCPClient:
202
- def __init__(self):
203
- self.session: mcp.ClientSession | None = None
204
- self.exit_stack = contextlib.AsyncExitStack()
205
-
206
- async def connect_to_server(self, config):
207
- """Connect to an MCP server."""
208
- server_params = mcp.StdioServerParameters(**config)
209
- stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
210
- self.stdio, self.write = stdio_transport
211
- self.session = await self.exit_stack.enter_async_context(
212
- mcp.ClientSession(self.stdio, self.write)
213
- )
214
- await self.session.initialize()
215
- self.connected = True
216
- response = await self.session.list_tools()
217
- self.tools = [self.to_func(tool) for tool in response.tools]
218
-
219
- async def run_tool(self, tool_name: str, tool_args: dict):
220
- print(f"Calling tool {tool_name} with args {tool_args}")
221
- return await self.session.call_tool(tool_name, tool_args)
222
-
223
- def to_func(self, tool):
224
- """Convert a tool to a callable function."""
225
-
226
- async def func(**kwargs):
227
- res = await self.run_tool(tool.name, kwargs)
228
- return res.content[0].text
229
-
230
- func.__name__ = tool.name
231
- func._tool = Tool(
232
- type="function",
233
- function=Function(
234
- name=tool.name,
235
- description=tool.description,
236
- parameters=tool.inputSchema,
237
- ),
238
- )
239
- return func
240
-
241
- async def cleanup(self):
242
- await self.exit_stack.aclose()
243
-
244
-
245
- class MultiMCPClient:
246
- """A client that can connect to multiple MCP servers."""
247
-
248
- async def connect(self, servers: list[str]):
249
- self.clients = []
250
- for server in servers:
251
- client = MCPClient()
252
- [command, *args] = server
253
- await client.connect_to_server({"command": command, "args": args})
254
- self.clients.append(client)
255
- tools = []
256
- for client in self.clients:
257
- tools.extend(client.tools)
258
- self.tools = tools
259
-
260
- async def cleanup(self):
261
- # Reverse cleanup as in https://github.com/microsoft/semantic-kernel/issues/12627.
262
- for client in reversed(self.clients):
263
- await client.cleanup()
 
 
1
  from lynxkite_core import ops
2
  from lynxkite_graph_analytics import core
 
 
 
 
 
 
 
 
 
3
 
4
  lsop = ops.op_registration(
5
  "LynxKite Graph Analytics", "LynxScribe", dir="bottom-to-top", color="blue"
 
7
  dsop = ops.op_registration("LynxKite Graph Analytics", "Data Science")
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  @lsop("MCP: Query database with SQL", color="green")
11
  def sql_tool(db: str):
12
  return {
 
24
  def db(data_pipeline: list[core.Bundle]):
25
  # The source of this file: https://github.com/biggraph/lynxscribe/pull/416
26
  return "movie_data.sqlite.db"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/LynxScribe/MCP/requirements.txt DELETED
@@ -1,3 +0,0 @@
1
- mcp
2
- streamlit
3
- ../../lynxscribe
 
 
 
 
lynxkite-lynxscribe/src/lynxkite_lynxscribe/__init__.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from . import lynxscribe_ops # noqa (imported to trigger registration)
2
  from . import llm_ops # noqa (imported to trigger registration)
3
  from .lynxscribe_ops import api_service_post, api_service_get
 
1
+ from . import agentic # noqa (imported to trigger registration)
2
  from . import lynxscribe_ops # noqa (imported to trigger registration)
3
  from . import llm_ops # noqa (imported to trigger registration)
4
  from .lynxscribe_ops import api_service_post, api_service_get
lynxkite-lynxscribe/src/lynxkite_lynxscribe/agentic.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Operations that allow LynxScribe agents and MCP servers to be combined with LynxKite pipelines."""
2
+
3
+ import enum
4
+ import os
5
+ import typing
6
+
7
+ from lynxkite_core import ops
8
+ from lynxscribe.components.task_solver import TaskSolver
9
+ from lynxscribe.components.mcp import MCPClient
10
+ from lynxscribe.core.llm.base import get_llm_engine
11
+ from lynxscribe.core.models.prompts import Function, Tool
12
+
13
+ if typing.TYPE_CHECKING:
14
+ import fastapi
15
+
16
+ CHAT_FRONTEND_URL = os.environ.get("CHAT_FRONTEND_URL", "http://localhost:8501/")
17
+
18
+ op = ops.op_registration(
19
+ "LynxKite Graph Analytics", "LynxScribe", dir="bottom-to-top", color="blue"
20
+ )
21
+
22
+
23
+ @op("Chat frontend", color="gray", outputs=[], view="service")
24
+ def chat_frontend(agent: dict):
25
+ agent = agent["agent"]
26
+ return Agent(
27
+ agent["name"],
28
+ agent["description"],
29
+ agent["system_prompt"],
30
+ agent["mcp_servers"],
31
+ [chat_frontend(a) for a in agent["sub_agents"]],
32
+ )
33
+
34
+
35
+ @op("Agent")
36
+ def agent(
37
+ tools: list[dict],
38
+ *,
39
+ name: str = "",
40
+ description: ops.LongStr = "This agent helps with various tasks.",
41
+ system_prompt: ops.LongStr = "You are a helpful assistant.",
42
+ ):
43
+ prompt = [system_prompt]
44
+ for tool in tools:
45
+ if tool.get("extra_prompt"):
46
+ prompt.append(tool["extra_prompt"])
47
+ return {
48
+ "agent": {
49
+ "name": name,
50
+ "description": description,
51
+ "system_prompt": "\n".join(prompt),
52
+ "mcp_servers": [t["command"] for t in tools if "command" in t],
53
+ "sub_agents": [t for t in tools if "agent" in t],
54
+ }
55
+ }
56
+
57
+
58
+ @op("MCP: Custom", color="green")
59
+ def custom_mcp(*, command: str, extra_prompt: ops.LongStr):
60
+ if command.strip():
61
+ return {"command": command.strip().split(), "extra_prompt": extra_prompt}
62
+
63
+
64
+ # A few basic MCP integrations to serve as examples.
65
+
66
+
67
+ class MCPSearchEngine(str, enum.Enum):
68
+ Google = "Google"
69
+ Bing = "Bing"
70
+ DuckDuckGo = "DuckDuckGo"
71
+
72
+
73
+ @op("MCP: Search web", color="green")
74
+ def web_search(*, engine: MCPSearchEngine = MCPSearchEngine.Google):
75
+ match engine:
76
+ case MCPSearchEngine.Google:
77
+ return {"command": ["npx", "-y", "https://github.com/pskill9/web-search"]}
78
+ case MCPSearchEngine.Bing:
79
+ return {"command": ["uvx", "bing-search-mcp"]}
80
+ case MCPSearchEngine.DuckDuckGo:
81
+ return {"command": ["uvx", "duckduckgo-mcp-server"]}
82
+
83
+
84
+ @op("MCP: Financial data", color="green")
85
+ def financial_data():
86
+ return {"command": ["uvx", "investor-agent"]}
87
+
88
+
89
+ @op("MCP: Calculator", color="green")
90
+ def calculator():
91
+ return {"command": ["uvx", "mcp-server-calculator"]}
92
+
93
+
94
+ @op("MCP: Time", color="green")
95
+ def time():
96
+ return {"command": ["uvx", "mcp-server-time"]}
97
+
98
+
99
+ class Agent:
100
+ def __init__(
101
+ self,
102
+ name: str,
103
+ description: str,
104
+ prompt: str,
105
+ mcp_servers: list[list[str]],
106
+ agents: list["Agent"],
107
+ ):
108
+ self.name = name
109
+ self.description = description
110
+ self.prompt = prompt
111
+ self.mcp_servers = mcp_servers
112
+ self.agents = agents
113
+ self.mcp_client = None
114
+ self.task_solver = None
115
+
116
+ async def init(self):
117
+ if self.task_solver is not None:
118
+ return
119
+ self.mcp_client = MCPClient()
120
+ await self.mcp_client.connect(self.mcp_servers)
121
+ agents_as_functions = [agent.as_function() for agent in self.agents]
122
+ self.task_solver = TaskSolver(
123
+ llm=get_llm_engine(name="openai"),
124
+ model="gpt-4.1-nano",
125
+ initial_messages=[self.prompt],
126
+ functions=[*self.mcp_client.tools, *agents_as_functions],
127
+ tool_choice="required",
128
+ temperature=0.0,
129
+ max_tool_call_steps=999,
130
+ )
131
+
132
+ def get_description(self, url: str) -> str:
133
+ return f"[Go to frontend]({CHAT_FRONTEND_URL}?service={url}/chat/completions)"
134
+
135
+ async def get(self, request: "fastapi.Request") -> dict:
136
+ if request.state.remaining_path == "models":
137
+ return {
138
+ "object": "list",
139
+ "data": [
140
+ {
141
+ "id": "LynxScribe",
142
+ "object": "model",
143
+ "created": 0,
144
+ "owned_by": "lynxkite",
145
+ "meta": {"profile_image_url": "https://lynxkite.com/favicon.png"},
146
+ }
147
+ ],
148
+ }
149
+ return {"error": "Not found"}
150
+
151
+ async def post(self, request: "fastapi.Request") -> dict:
152
+ if request.state.remaining_path == "chat/completions":
153
+ request = await request.json()
154
+ assert not request["stream"]
155
+ await self.init()
156
+ res = await self.task_solver.solve(request["messages"][-1]["content"])
157
+ return {"choices": [{"message": {"role": "assistant", "content": res}}]}
158
+
159
+ return {"error": "Not found"}
160
+
161
+ def as_function(self):
162
+ """A callable that can be used as a tool by another Agent."""
163
+
164
+ # Find the value of x given that 4*x^4 = 44. Compute the numerical value.
165
+ async def ask(message):
166
+ print(f"Calling agent {self.name} with message: {message}")
167
+ await self.init()
168
+ res = await self.task_solver.solve(message)
169
+ print(f"Agent {self.name} response: {res}")
170
+ return res
171
+
172
+ ask.__name__ = "ask_" + self.name.lower().replace(" ", "_")
173
+ ask._tool = Tool(
174
+ type="function",
175
+ function=Function(
176
+ name=ask.__name__,
177
+ description=self.description,
178
+ parameters={
179
+ "type": "object",
180
+ "required": ["message"],
181
+ "properties": {
182
+ "message": {
183
+ "type": "string",
184
+ "description": "The question to ask the agent.",
185
+ }
186
+ },
187
+ },
188
+ ),
189
+ )
190
+ return ask