agno 1.7.6__py3-none-any.whl → 1.7.7__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.
- agno/agent/agent.py +5 -23
- agno/app/playground/app.py +3 -2
- agno/embedder/openai.py +5 -1
- agno/models/anthropic/claude.py +9 -1
- agno/models/google/gemini.py +26 -6
- agno/models/litellm/chat.py +26 -4
- agno/models/message.py +1 -0
- agno/storage/mysql.py +1 -0
- agno/storage/postgres.py +4 -3
- agno/team/team.py +6 -19
- agno/tools/jina.py +13 -6
- agno/tools/mcp.py +160 -23
- agno/tools/models/morph.py +186 -0
- agno/tools/postgres.py +31 -26
- agno/tools/zep.py +21 -32
- agno/utils/models/claude.py +1 -0
- agno/workflow/v2/workflow.py +6 -6
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/METADATA +8 -6
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/RECORD +23 -22
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/WHEEL +0 -0
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/entry_points.txt +0 -0
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/licenses/LICENSE +0 -0
- {agno-1.7.6.dist-info → agno-1.7.7.dist-info}/top_level.txt +0 -0
agno/tools/mcp.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import weakref
|
|
1
3
|
from contextlib import AsyncExitStack
|
|
2
4
|
from dataclasses import asdict, dataclass
|
|
3
5
|
from datetime import timedelta
|
|
@@ -18,6 +20,28 @@ except (ImportError, ModuleNotFoundError):
|
|
|
18
20
|
raise ImportError("`mcp` not installed. Please install using `pip install mcp`")
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
def _prepare_command(command: str) -> list[str]:
|
|
24
|
+
"""Sanitize a command and split it into parts before using it to run a MCP server."""
|
|
25
|
+
from shlex import split
|
|
26
|
+
|
|
27
|
+
# Block dangerous characters
|
|
28
|
+
if any(char in command for char in ["&", "|", ";", "`", "$", "(", ")"]):
|
|
29
|
+
raise ValueError("MCP command can't contain shell metacharacters")
|
|
30
|
+
|
|
31
|
+
parts = split(command)
|
|
32
|
+
if not parts:
|
|
33
|
+
raise ValueError("MCP command can't be empty")
|
|
34
|
+
|
|
35
|
+
# Only allow specific executables
|
|
36
|
+
ALLOWED_COMMANDS = {"python", "python3", "node", "npm", "npx"}
|
|
37
|
+
|
|
38
|
+
executable = parts[0].split("/")[-1]
|
|
39
|
+
if executable not in ALLOWED_COMMANDS:
|
|
40
|
+
raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
|
|
41
|
+
|
|
42
|
+
return parts
|
|
43
|
+
|
|
44
|
+
|
|
21
45
|
@dataclass
|
|
22
46
|
class SSEClientParams:
|
|
23
47
|
"""Parameters for SSE client connection."""
|
|
@@ -138,11 +162,7 @@ class MCPTools(Toolkit):
|
|
|
138
162
|
env = get_default_environment()
|
|
139
163
|
|
|
140
164
|
if command is not None and transport not in ["sse", "streamable-http"]:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
parts = split(command)
|
|
144
|
-
if not parts:
|
|
145
|
-
raise ValueError("Empty command string")
|
|
165
|
+
parts = _prepare_command(command)
|
|
146
166
|
cmd = parts[0]
|
|
147
167
|
arguments = parts[1:] if len(parts) > 1 else []
|
|
148
168
|
self.server_params = StdioServerParameters(command=cmd, args=arguments, env=env)
|
|
@@ -151,23 +171,49 @@ class MCPTools(Toolkit):
|
|
|
151
171
|
self._context = None
|
|
152
172
|
self._session_context = None
|
|
153
173
|
self._initialized = False
|
|
174
|
+
self._connection_task = None
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
176
|
+
def cleanup():
|
|
177
|
+
"""Cancel active connections"""
|
|
178
|
+
if self._connection_task and not self._connection_task.done():
|
|
179
|
+
self._connection_task.cancel()
|
|
180
|
+
|
|
181
|
+
# Setup cleanup logic before the instance is garbage collected
|
|
182
|
+
self._cleanup_finalizer = weakref.finalize(self, cleanup)
|
|
183
|
+
|
|
184
|
+
async def connect(self):
|
|
185
|
+
"""Initialize a MCPTools instance and connect to the contextual MCP server"""
|
|
186
|
+
if self._initialized:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
await self._connect()
|
|
190
|
+
|
|
191
|
+
def _start_connection(self):
|
|
192
|
+
"""Ensure there are no active connections and setup a new one"""
|
|
193
|
+
if self._connection_task is None or self._connection_task.done():
|
|
194
|
+
self._connection_task = asyncio.create_task(self._connect()) # type: ignore
|
|
195
|
+
|
|
196
|
+
async def _connect(self) -> None:
|
|
197
|
+
"""Connects to the MCP server and initializes the tools"""
|
|
198
|
+
if self._initialized:
|
|
199
|
+
return
|
|
157
200
|
|
|
158
201
|
if self.session is not None:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
await self.initialize()
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if not hasattr(self, "_active_contexts"):
|
|
206
|
+
self._active_contexts: list[Any] = []
|
|
163
207
|
|
|
164
|
-
# Create a new session
|
|
208
|
+
# Create a new studio session
|
|
165
209
|
if self.transport == "sse":
|
|
166
210
|
sse_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
|
|
167
211
|
if "url" not in sse_params:
|
|
168
212
|
sse_params["url"] = self.url
|
|
169
213
|
self._context = sse_client(**sse_params) # type: ignore
|
|
170
214
|
client_timeout = min(self.timeout_seconds, sse_params.get("timeout", self.timeout_seconds))
|
|
215
|
+
|
|
216
|
+
# Create a new streamable HTTP session
|
|
171
217
|
elif self.transport == "streamable-http":
|
|
172
218
|
streamable_http_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
|
|
173
219
|
if "url" not in streamable_http_params:
|
|
@@ -177,6 +223,7 @@ class MCPTools(Toolkit):
|
|
|
177
223
|
if isinstance(params_timeout, timedelta):
|
|
178
224
|
params_timeout = int(params_timeout.total_seconds())
|
|
179
225
|
client_timeout = min(self.timeout_seconds, params_timeout)
|
|
226
|
+
|
|
180
227
|
else:
|
|
181
228
|
if self.server_params is None:
|
|
182
229
|
raise ValueError("server_params must be provided when using stdio transport.")
|
|
@@ -184,24 +231,42 @@ class MCPTools(Toolkit):
|
|
|
184
231
|
client_timeout = self.timeout_seconds
|
|
185
232
|
|
|
186
233
|
session_params = await self._context.__aenter__() # type: ignore
|
|
234
|
+
self._active_contexts.append(self._context)
|
|
187
235
|
read, write = session_params[0:2]
|
|
188
236
|
|
|
189
237
|
self._session_context = ClientSession(read, write, read_timeout_seconds=timedelta(seconds=client_timeout)) # type: ignore
|
|
190
238
|
self.session = await self._session_context.__aenter__() # type: ignore
|
|
239
|
+
self._active_contexts.append(self._session_context)
|
|
191
240
|
|
|
192
241
|
# Initialize with the new session
|
|
193
242
|
await self.initialize()
|
|
243
|
+
|
|
244
|
+
async def close(self) -> None:
|
|
245
|
+
"""Close the MCP connection and clean up resources"""
|
|
246
|
+
if self._session_context is not None:
|
|
247
|
+
await self._session_context.__aexit__(None, None, None)
|
|
248
|
+
self.session = None
|
|
249
|
+
self._session_context = None
|
|
250
|
+
|
|
251
|
+
if self._context is not None:
|
|
252
|
+
await self._context.__aexit__(None, None, None)
|
|
253
|
+
self._context = None
|
|
254
|
+
|
|
255
|
+
self._initialized = False
|
|
256
|
+
|
|
257
|
+
async def __aenter__(self) -> "MCPTools":
|
|
258
|
+
await self._connect()
|
|
194
259
|
return self
|
|
195
260
|
|
|
196
|
-
async def __aexit__(self,
|
|
261
|
+
async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
|
|
197
262
|
"""Exit the async context manager."""
|
|
198
263
|
if self._session_context is not None:
|
|
199
|
-
await self._session_context.__aexit__(
|
|
264
|
+
await self._session_context.__aexit__(_exc_type, _exc_val, _exc_tb)
|
|
200
265
|
self.session = None
|
|
201
266
|
self._session_context = None
|
|
202
267
|
|
|
203
268
|
if self._context is not None:
|
|
204
|
-
await self._context.__aexit__(
|
|
269
|
+
await self._context.__aexit__(_exc_type, _exc_val, _exc_tb)
|
|
205
270
|
self._context = None
|
|
206
271
|
|
|
207
272
|
self._initialized = False
|
|
@@ -213,7 +278,7 @@ class MCPTools(Toolkit):
|
|
|
213
278
|
|
|
214
279
|
try:
|
|
215
280
|
if self.session is None:
|
|
216
|
-
raise ValueError("
|
|
281
|
+
raise ValueError("Failed to establish session connection")
|
|
217
282
|
|
|
218
283
|
# Initialize the session if not already initialized
|
|
219
284
|
await self.session.initialize()
|
|
@@ -343,12 +408,8 @@ class MultiMCPTools(Toolkit):
|
|
|
343
408
|
env = get_default_environment()
|
|
344
409
|
|
|
345
410
|
if commands is not None:
|
|
346
|
-
from shlex import split
|
|
347
|
-
|
|
348
411
|
for command in commands:
|
|
349
|
-
parts =
|
|
350
|
-
if not parts:
|
|
351
|
-
raise ValueError("Empty command string")
|
|
412
|
+
parts = _prepare_command(command)
|
|
352
413
|
cmd = parts[0]
|
|
353
414
|
arguments = parts[1:] if len(parts) > 1 else []
|
|
354
415
|
self.server_params_list.append(StdioServerParameters(command=cmd, args=arguments, env=env))
|
|
@@ -365,28 +426,92 @@ class MultiMCPTools(Toolkit):
|
|
|
365
426
|
self.server_params_list.append(StreamableHTTPClientParams(url=url))
|
|
366
427
|
|
|
367
428
|
self._async_exit_stack = AsyncExitStack()
|
|
429
|
+
self._initialized = False
|
|
430
|
+
self._connection_task = None
|
|
431
|
+
self._active_contexts: list[Any] = []
|
|
432
|
+
self._used_as_context_manager = False
|
|
368
433
|
|
|
369
434
|
self._client = client
|
|
370
435
|
|
|
371
|
-
|
|
372
|
-
|
|
436
|
+
def cleanup():
|
|
437
|
+
"""Cancel active connections"""
|
|
438
|
+
if self._connection_task and not self._connection_task.done():
|
|
439
|
+
self._connection_task.cancel()
|
|
440
|
+
|
|
441
|
+
# Setup cleanup logic before the instance is garbage collected
|
|
442
|
+
self._cleanup_finalizer = weakref.finalize(self, cleanup)
|
|
443
|
+
|
|
444
|
+
async def connect(self):
|
|
445
|
+
"""Initialize a MultiMCPTools instance and connect to the MCP servers"""
|
|
446
|
+
if self._initialized:
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
await self._connect()
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
async def create_and_connect(
|
|
453
|
+
cls,
|
|
454
|
+
commands: Optional[List[str]] = None,
|
|
455
|
+
urls: Optional[List[str]] = None,
|
|
456
|
+
urls_transports: Optional[List[Literal["sse", "streamable-http"]]] = None,
|
|
457
|
+
*,
|
|
458
|
+
env: Optional[dict[str, str]] = None,
|
|
459
|
+
server_params_list: Optional[
|
|
460
|
+
List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
|
|
461
|
+
] = None,
|
|
462
|
+
timeout_seconds: int = 5,
|
|
463
|
+
client=None,
|
|
464
|
+
include_tools: Optional[list[str]] = None,
|
|
465
|
+
exclude_tools: Optional[list[str]] = None,
|
|
466
|
+
**kwargs,
|
|
467
|
+
) -> "MultiMCPTools":
|
|
468
|
+
"""Initialize a MultiMCPTools instance and connect to the MCP servers"""
|
|
469
|
+
instance = cls(
|
|
470
|
+
commands=commands,
|
|
471
|
+
urls=urls,
|
|
472
|
+
urls_transports=urls_transports,
|
|
473
|
+
env=env,
|
|
474
|
+
server_params_list=server_params_list,
|
|
475
|
+
timeout_seconds=timeout_seconds,
|
|
476
|
+
client=client,
|
|
477
|
+
include_tools=include_tools,
|
|
478
|
+
exclude_tools=exclude_tools,
|
|
479
|
+
**kwargs,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
await instance._connect()
|
|
483
|
+
return instance
|
|
484
|
+
|
|
485
|
+
def _start_connection(self):
|
|
486
|
+
"""Ensure there are no active connections and setup a new one"""
|
|
487
|
+
if self._connection_task is None or self._connection_task.done():
|
|
488
|
+
self._connection_task = asyncio.create_task(self._connect()) # type: ignore
|
|
489
|
+
|
|
490
|
+
async def _connect(self) -> None:
|
|
491
|
+
"""Connects to the MCP servers and initializes the tools"""
|
|
492
|
+
if self._initialized:
|
|
493
|
+
return
|
|
373
494
|
|
|
374
495
|
for server_params in self.server_params_list:
|
|
375
496
|
# Handle stdio connections
|
|
376
497
|
if isinstance(server_params, StdioServerParameters):
|
|
377
498
|
stdio_transport = await self._async_exit_stack.enter_async_context(stdio_client(server_params))
|
|
499
|
+
self._active_contexts.append(stdio_transport)
|
|
378
500
|
read, write = stdio_transport
|
|
379
501
|
session = await self._async_exit_stack.enter_async_context(
|
|
380
502
|
ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout_seconds))
|
|
381
503
|
)
|
|
504
|
+
self._active_contexts.append(session)
|
|
382
505
|
await self.initialize(session)
|
|
383
506
|
# Handle SSE connections
|
|
384
507
|
elif isinstance(server_params, SSEClientParams):
|
|
385
508
|
client_connection = await self._async_exit_stack.enter_async_context(
|
|
386
509
|
sse_client(**asdict(server_params))
|
|
387
510
|
)
|
|
511
|
+
self._active_contexts.append(client_connection)
|
|
388
512
|
read, write = client_connection
|
|
389
513
|
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
514
|
+
self._active_contexts.append(session)
|
|
390
515
|
await self.initialize(session)
|
|
391
516
|
|
|
392
517
|
# Handle Streamable HTTP connections
|
|
@@ -394,10 +519,22 @@ class MultiMCPTools(Toolkit):
|
|
|
394
519
|
client_connection = await self._async_exit_stack.enter_async_context(
|
|
395
520
|
streamablehttp_client(**asdict(server_params))
|
|
396
521
|
)
|
|
522
|
+
self._active_contexts.append(client_connection)
|
|
397
523
|
read, write = client_connection[0:2]
|
|
398
524
|
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
525
|
+
self._active_contexts.append(session)
|
|
399
526
|
await self.initialize(session)
|
|
400
527
|
|
|
528
|
+
self._initialized = True
|
|
529
|
+
|
|
530
|
+
async def close(self) -> None:
|
|
531
|
+
"""Close the MCP connections and clean up resources"""
|
|
532
|
+
await self._async_exit_stack.aclose()
|
|
533
|
+
self._initialized = False
|
|
534
|
+
|
|
535
|
+
async def __aenter__(self) -> "MultiMCPTools":
|
|
536
|
+
"""Enter the async context manager."""
|
|
537
|
+
await self._connect()
|
|
401
538
|
return self
|
|
402
539
|
|
|
403
540
|
async def __aexit__(
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from os import getenv
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from agno.tools import Toolkit
|
|
7
|
+
from agno.utils.log import log_debug, log_error
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from openai import OpenAI
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError("`openai` not installed. Please install using `pip install openai`")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MorphTools(Toolkit):
|
|
16
|
+
"""Tools for interacting with Morph's Fast Apply API for code editing"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
base_url: str = "https://api.morphllm.com/v1",
|
|
22
|
+
instructions: Optional[str] = None,
|
|
23
|
+
add_instructions: bool = True,
|
|
24
|
+
model: str = "morph-v3-large",
|
|
25
|
+
**kwargs,
|
|
26
|
+
):
|
|
27
|
+
"""Initialize Morph Fast Apply tools.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Morph API key. If not provided, will look for MORPH_API_KEY environment variable.
|
|
31
|
+
base_url: The base URL for the Morph API.
|
|
32
|
+
model: The Morph model to use. Options:
|
|
33
|
+
- "morph-v3-fast" (4500+ tok/sec, 96% accuracy)
|
|
34
|
+
- "morph-v3-large" (2500+ tok/sec, 98% accuracy)
|
|
35
|
+
- "auto" (automatic selection)
|
|
36
|
+
**kwargs: Additional arguments to pass to Toolkit.
|
|
37
|
+
"""
|
|
38
|
+
# Set up instructions
|
|
39
|
+
if instructions is None:
|
|
40
|
+
self.instructions = self.DEFAULT_INSTRUCTIONS
|
|
41
|
+
else:
|
|
42
|
+
self.instructions = instructions
|
|
43
|
+
|
|
44
|
+
super().__init__(
|
|
45
|
+
name="morph_tools",
|
|
46
|
+
tools=[self.edit_file],
|
|
47
|
+
instructions=self.instructions,
|
|
48
|
+
add_instructions=add_instructions,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.api_key = api_key or getenv("MORPH_API_KEY")
|
|
53
|
+
if not self.api_key:
|
|
54
|
+
raise ValueError("MORPH_API_KEY not set. Please set the MORPH_API_KEY environment variable.")
|
|
55
|
+
|
|
56
|
+
self.base_url = base_url
|
|
57
|
+
self.model = model
|
|
58
|
+
self._morph_client: Optional[OpenAI] = None
|
|
59
|
+
|
|
60
|
+
def _get_client(self):
|
|
61
|
+
"""Get or create the Morph OpenAI client."""
|
|
62
|
+
if self._morph_client is None:
|
|
63
|
+
self._morph_client = OpenAI(
|
|
64
|
+
api_key=self.api_key,
|
|
65
|
+
base_url=self.base_url,
|
|
66
|
+
)
|
|
67
|
+
return self._morph_client
|
|
68
|
+
|
|
69
|
+
def edit_file(
|
|
70
|
+
self,
|
|
71
|
+
target_file: str,
|
|
72
|
+
instructions: str,
|
|
73
|
+
code_edit: str,
|
|
74
|
+
original_code: Optional[str] = None,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Apply code edits to a target file using Morph's Fast Apply API.
|
|
78
|
+
|
|
79
|
+
This function reads the specified file, sends its content along with
|
|
80
|
+
editing instructions and code edits to Morph's API, and writes the
|
|
81
|
+
resulting code back to the file. A backup of the original file is
|
|
82
|
+
created before writing changes.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
target_file (str): Path to the file to be edited.
|
|
86
|
+
instructions (str): High-level instructions describing the intended change.
|
|
87
|
+
code_edit (str): Specific code edit or change to apply.
|
|
88
|
+
original_code (Optional[str], optional): Original content of the file.
|
|
89
|
+
If not provided, the function reads from target_file.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
str: Result message indicating success or failure, and details about
|
|
93
|
+
the backup and any errors encountered.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Always read the actual file content for backup purposes
|
|
97
|
+
actual_file_content = None
|
|
98
|
+
if os.path.exists(target_file):
|
|
99
|
+
try:
|
|
100
|
+
with open(target_file, "r", encoding="utf-8") as f:
|
|
101
|
+
actual_file_content = f.read()
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return f"Error reading {target_file} for backup: {e}"
|
|
104
|
+
else:
|
|
105
|
+
return f"Error: File {target_file} does not exist."
|
|
106
|
+
|
|
107
|
+
# Use provided original_code or fall back to file content
|
|
108
|
+
code_to_process = original_code if original_code is not None else actual_file_content
|
|
109
|
+
|
|
110
|
+
# Format the message for Morph's Fast Apply API
|
|
111
|
+
content = f"<instruction>{instructions}</instruction>\n<code>{code_to_process}</code>\n<update>{code_edit}</update>"
|
|
112
|
+
|
|
113
|
+
log_debug(f"Input to Morph: {content}")
|
|
114
|
+
|
|
115
|
+
client = self._get_client()
|
|
116
|
+
|
|
117
|
+
response = client.chat.completions.create(
|
|
118
|
+
model=self.model,
|
|
119
|
+
messages=[
|
|
120
|
+
{
|
|
121
|
+
"role": "user",
|
|
122
|
+
"content": content,
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if response.choices and response.choices[0].message.content:
|
|
128
|
+
final_code = response.choices[0].message.content
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
backup_file = f"{target_file}.backup"
|
|
132
|
+
with open(backup_file, "w", encoding="utf-8") as f:
|
|
133
|
+
f.write(actual_file_content)
|
|
134
|
+
|
|
135
|
+
# Write the new code
|
|
136
|
+
with open(target_file, "w", encoding="utf-8") as f:
|
|
137
|
+
f.write(final_code)
|
|
138
|
+
return f"Successfully applied edit to {target_file} using Morph Fast Apply! Original content backed up as {backup_file}"
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return f"Successfully applied edit but failed to write back to {target_file}: {e}"
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
return f"Failed to apply edit to {target_file}: No response from Morph API"
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
log_error(f"Failed to apply edit using Morph Fast Apply: {e}")
|
|
148
|
+
return f"Failed to apply edit to {target_file}: {e}"
|
|
149
|
+
|
|
150
|
+
DEFAULT_INSTRUCTIONS = dedent("""\
|
|
151
|
+
You have access to Morph Fast Apply for ultra-fast code editing with 98% accuracy at 2500+ tokens/second.
|
|
152
|
+
|
|
153
|
+
## How to use the edit_file tool:
|
|
154
|
+
|
|
155
|
+
**Critical Requirements:**
|
|
156
|
+
1. **Instructions Parameter**: Generate clear first-person instructions describing what you're doing
|
|
157
|
+
- Example: "I am adding type hints to all functions and methods"
|
|
158
|
+
- Example: "I am refactoring the error handling to use try-catch blocks"
|
|
159
|
+
|
|
160
|
+
2. **Code Edit Parameter**: Specify ONLY the lines you want to change
|
|
161
|
+
- Use `# ... existing code ...` (or `// ... existing code ...` for JS/Java) to represent unchanged sections
|
|
162
|
+
- NEVER write out unchanged code in the code_edit parameter
|
|
163
|
+
- Include sufficient context around changes to resolve ambiguity
|
|
164
|
+
|
|
165
|
+
3. **Single Edit Call**: Make ALL edits to a file in a single edit_file call. The apply model can handle many distinct edits at once.
|
|
166
|
+
|
|
167
|
+
**Example Format:**
|
|
168
|
+
```
|
|
169
|
+
# ... existing code ...
|
|
170
|
+
def add(a: int, b: int) -> int:
|
|
171
|
+
\"\"\"Add two numbers together.\"\"\"
|
|
172
|
+
return a + b
|
|
173
|
+
# ... existing code ...
|
|
174
|
+
def multiply(x: int, y: int) -> int:
|
|
175
|
+
\"\"\"Multiply two numbers.\"\"\"
|
|
176
|
+
return x * y
|
|
177
|
+
# ... existing code ...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Important Guidelines:**
|
|
181
|
+
- Bias towards repeating as few lines as possible while conveying the change clearly
|
|
182
|
+
- Each edit should contain sufficient context of unchanged lines around the code you're editing
|
|
183
|
+
- DO NOT omit spans of pre-existing code without using the `# ... existing code ...` comment
|
|
184
|
+
- If deleting a section, provide context before and after to clearly indicate the deletion
|
|
185
|
+
- The tool automatically creates backup files before applying changes\
|
|
186
|
+
""")
|
agno/tools/postgres.py
CHANGED
|
@@ -2,14 +2,12 @@ import csv
|
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
|
3
3
|
|
|
4
4
|
try:
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
5
|
+
import psycopg
|
|
6
|
+
from psycopg import sql
|
|
7
|
+
from psycopg.connection import Connection as PgConnection
|
|
8
|
+
from psycopg.rows import DictRow, dict_row
|
|
9
9
|
except ImportError:
|
|
10
|
-
raise ImportError(
|
|
11
|
-
"`psycopg2` not installed. Please install using `pip install psycopg2`. If you face issues, try `pip install psycopg2-binary`."
|
|
12
|
-
)
|
|
10
|
+
raise ImportError("`psycopg` not installed. Please install using `pip install 'psycopg-binary'`.")
|
|
13
11
|
|
|
14
12
|
from agno.tools import Toolkit
|
|
15
13
|
from agno.utils.log import log_debug, log_error
|
|
@@ -18,7 +16,7 @@ from agno.utils.log import log_debug, log_error
|
|
|
18
16
|
class PostgresTools(Toolkit):
|
|
19
17
|
def __init__(
|
|
20
18
|
self,
|
|
21
|
-
connection: Optional[PgConnection] = None,
|
|
19
|
+
connection: Optional[PgConnection[DictRow]] = None,
|
|
22
20
|
db_name: Optional[str] = None,
|
|
23
21
|
user: Optional[str] = None,
|
|
24
22
|
password: Optional[str] = None,
|
|
@@ -31,7 +29,7 @@ class PostgresTools(Toolkit):
|
|
|
31
29
|
table_schema: str = "public",
|
|
32
30
|
**kwargs,
|
|
33
31
|
):
|
|
34
|
-
self._connection: Optional[PgConnection] = connection
|
|
32
|
+
self._connection: Optional[PgConnection[DictRow]] = connection
|
|
35
33
|
self.db_name: Optional[str] = db_name
|
|
36
34
|
self.user: Optional[str] = user
|
|
37
35
|
self.password: Optional[str] = password
|
|
@@ -55,16 +53,16 @@ class PostgresTools(Toolkit):
|
|
|
55
53
|
super().__init__(name="postgres_tools", tools=tools, **kwargs)
|
|
56
54
|
|
|
57
55
|
@property
|
|
58
|
-
def connection(self) -> PgConnection:
|
|
56
|
+
def connection(self) -> PgConnection[DictRow]:
|
|
59
57
|
"""
|
|
60
|
-
Returns the Postgres
|
|
61
|
-
:return
|
|
58
|
+
Returns the Postgres psycopg connection.
|
|
59
|
+
:return psycopg.connection.Connection: psycopg connection
|
|
62
60
|
"""
|
|
63
61
|
if self._connection is None or self._connection.closed:
|
|
64
62
|
log_debug("Establishing new PostgreSQL connection.")
|
|
65
|
-
connection_kwargs: Dict[str, Any] = {"
|
|
63
|
+
connection_kwargs: Dict[str, Any] = {"row_factory": dict_row}
|
|
66
64
|
if self.db_name:
|
|
67
|
-
connection_kwargs["
|
|
65
|
+
connection_kwargs["dbname"] = self.db_name
|
|
68
66
|
if self.user:
|
|
69
67
|
connection_kwargs["user"] = self.user
|
|
70
68
|
if self.password:
|
|
@@ -76,8 +74,8 @@ class PostgresTools(Toolkit):
|
|
|
76
74
|
|
|
77
75
|
connection_kwargs["options"] = f"-c search_path={self.table_schema}"
|
|
78
76
|
|
|
79
|
-
self._connection =
|
|
80
|
-
self._connection.
|
|
77
|
+
self._connection = psycopg.connect(**connection_kwargs)
|
|
78
|
+
self._connection.read_only = True
|
|
81
79
|
|
|
82
80
|
return self._connection
|
|
83
81
|
|
|
@@ -110,10 +108,10 @@ class PostgresTools(Toolkit):
|
|
|
110
108
|
return f"Query returned no results.\nColumns: {', '.join(columns)}"
|
|
111
109
|
|
|
112
110
|
header = ",".join(columns)
|
|
113
|
-
data_rows = [",".join(map(str, row)) for row in rows]
|
|
111
|
+
data_rows = [",".join(map(str, row.values())) for row in rows]
|
|
114
112
|
return f"{header}\n" + "\n".join(data_rows)
|
|
115
113
|
|
|
116
|
-
except
|
|
114
|
+
except psycopg.Error as e:
|
|
117
115
|
log_error(f"Database error: {e}")
|
|
118
116
|
if self.connection and not self.connection.closed:
|
|
119
117
|
self.connection.rollback()
|
|
@@ -203,15 +201,18 @@ class PostgresTools(Toolkit):
|
|
|
203
201
|
cursor.execute(query)
|
|
204
202
|
stats = cursor.fetchone()
|
|
205
203
|
summary_parts.append(f"\n--- Column: {col_name} (Type: {data_type}) ---")
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
if stats is not None:
|
|
205
|
+
for key, value in stats.items():
|
|
206
|
+
val_str = (
|
|
207
|
+
f"{value:.2f}" if isinstance(value, float) and value is not None else str(value)
|
|
208
|
+
)
|
|
209
|
+
summary_parts.append(f" {key}: {val_str}")
|
|
210
|
+
else:
|
|
211
|
+
summary_parts.append(" No statistics available")
|
|
211
212
|
|
|
212
213
|
return "\n".join(summary_parts)
|
|
213
214
|
|
|
214
|
-
except
|
|
215
|
+
except psycopg.Error as e:
|
|
215
216
|
return f"Error summarizing table: {e}"
|
|
216
217
|
|
|
217
218
|
def inspect_query(self, query: str) -> str:
|
|
@@ -239,15 +240,19 @@ class PostgresTools(Toolkit):
|
|
|
239
240
|
try:
|
|
240
241
|
with self.connection.cursor() as cursor:
|
|
241
242
|
cursor.execute(stmt)
|
|
243
|
+
|
|
244
|
+
if cursor.description is None:
|
|
245
|
+
return f"Error: Query returned no description for table '{table}'."
|
|
246
|
+
|
|
242
247
|
columns = [desc[0] for desc in cursor.description]
|
|
243
248
|
|
|
244
249
|
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
245
250
|
writer = csv.writer(f)
|
|
246
251
|
writer.writerow(columns)
|
|
247
|
-
writer.writerows(cursor)
|
|
252
|
+
writer.writerows(row.values() for row in cursor)
|
|
248
253
|
|
|
249
254
|
return f"Successfully exported table '{table}' to '{path}'."
|
|
250
|
-
except (
|
|
255
|
+
except (psycopg.Error, IOError) as e:
|
|
251
256
|
return f"Error exporting table: {e}"
|
|
252
257
|
|
|
253
258
|
def run_query(self, query: str) -> str:
|