agno 2.3.26__py3-none-any.whl → 2.4.1__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/__init__.py +4 -0
- agno/agent/agent.py +1368 -541
- agno/agent/remote.py +13 -0
- agno/db/base.py +339 -0
- agno/db/postgres/async_postgres.py +116 -12
- agno/db/postgres/postgres.py +1242 -25
- agno/db/postgres/schemas.py +48 -1
- agno/db/sqlite/async_sqlite.py +119 -4
- agno/db/sqlite/schemas.py +51 -0
- agno/db/sqlite/sqlite.py +1186 -13
- agno/db/utils.py +37 -1
- agno/integrations/discord/client.py +12 -1
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +1 -1
- agno/knowledge/chunking/semantic.py +1 -1
- agno/knowledge/chunking/strategy.py +4 -0
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +3722 -2182
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +2 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +236 -13
- agno/knowledge/reader/docx_reader.py +2 -2
- agno/knowledge/reader/field_labeled_csv_reader.py +169 -5
- agno/knowledge/reader/firecrawl_reader.py +2 -2
- agno/knowledge/reader/json_reader.py +2 -2
- agno/knowledge/reader/markdown_reader.py +2 -2
- agno/knowledge/reader/pdf_reader.py +5 -4
- agno/knowledge/reader/pptx_reader.py +2 -2
- agno/knowledge/reader/reader_factory.py +118 -1
- agno/knowledge/reader/s3_reader.py +2 -2
- agno/knowledge/reader/tavily_reader.py +2 -2
- agno/knowledge/reader/text_reader.py +2 -2
- agno/knowledge/reader/web_search_reader.py +2 -2
- agno/knowledge/reader/website_reader.py +5 -3
- agno/knowledge/reader/wikipedia_reader.py +2 -2
- agno/knowledge/reader/youtube_reader.py +2 -2
- agno/knowledge/remote_content/__init__.py +29 -0
- agno/knowledge/remote_content/config.py +204 -0
- agno/knowledge/remote_content/remote_content.py +74 -17
- agno/knowledge/utils.py +37 -29
- agno/learn/__init__.py +6 -0
- agno/learn/machine.py +35 -0
- agno/learn/schemas.py +82 -11
- agno/learn/stores/__init__.py +3 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/learned_knowledge.py +6 -6
- agno/models/anthropic/claude.py +24 -0
- agno/models/aws/bedrock.py +20 -0
- agno/models/base.py +60 -6
- agno/models/cerebras/cerebras.py +34 -2
- agno/models/cohere/chat.py +25 -0
- agno/models/google/gemini.py +50 -5
- agno/models/litellm/chat.py +38 -0
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/openai/chat.py +25 -1
- agno/models/openrouter/openrouter.py +46 -0
- agno/models/perplexity/perplexity.py +2 -0
- agno/models/response.py +16 -0
- agno/os/app.py +83 -44
- agno/os/interfaces/slack/router.py +10 -1
- agno/os/interfaces/whatsapp/router.py +6 -0
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +1 -0
- agno/os/routers/agents/router.py +29 -16
- agno/os/routers/agents/schema.py +6 -4
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/evals/schemas.py +4 -3
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +128 -3
- agno/os/routers/knowledge/schemas.py +12 -0
- agno/os/routers/memory/schemas.py +4 -2
- agno/os/routers/metrics/metrics.py +9 -11
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/teams/router.py +20 -8
- agno/os/routers/teams/schema.py +6 -4
- agno/os/routers/traces/traces.py +5 -5
- agno/os/routers/workflows/router.py +38 -11
- agno/os/routers/workflows/schema.py +1 -1
- agno/os/schema.py +92 -26
- agno/os/utils.py +84 -19
- agno/reasoning/anthropic.py +2 -2
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +2 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +4 -10
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +2 -2
- agno/reasoning/vertexai.py +2 -2
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/run/agent.py +59 -0
- agno/run/base.py +7 -0
- agno/run/team.py +57 -0
- agno/skills/agent_skills.py +10 -3
- agno/team/__init__.py +3 -1
- agno/team/team.py +1165 -330
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/function.py +35 -83
- agno/tools/knowledge.py +9 -4
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/parallel.py +0 -7
- agno/tools/reasoning.py +30 -23
- agno/tools/tavily.py +4 -1
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +48 -47
- agno/utils/agent.py +42 -5
- agno/utils/events.py +160 -2
- agno/utils/print_response/agent.py +0 -31
- agno/utils/print_response/team.py +0 -2
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/team.py +61 -11
- agno/vectordb/lancedb/lance_db.py +4 -1
- agno/vectordb/mongodb/mongodb.py +1 -1
- agno/vectordb/pgvector/pgvector.py +3 -3
- agno/vectordb/qdrant/qdrant.py +4 -4
- agno/workflow/__init__.py +3 -1
- agno/workflow/condition.py +0 -21
- agno/workflow/loop.py +0 -21
- agno/workflow/parallel.py +0 -21
- agno/workflow/router.py +0 -21
- agno/workflow/step.py +117 -24
- agno/workflow/steps.py +0 -21
- agno/workflow/workflow.py +427 -63
- {agno-2.3.26.dist-info → agno-2.4.1.dist-info}/METADATA +49 -76
- {agno-2.3.26.dist-info → agno-2.4.1.dist-info}/RECORD +140 -126
- {agno-2.3.26.dist-info → agno-2.4.1.dist-info}/WHEEL +1 -1
- {agno-2.3.26.dist-info → agno-2.4.1.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.26.dist-info → agno-2.4.1.dist-info}/top_level.txt +0 -0
agno/db/utils.py
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import date, datetime
|
|
5
|
-
from typing import Any, Dict
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
from agno.models.message import Message
|
|
9
9
|
from agno.models.metrics import Metrics
|
|
10
|
+
from agno.utils.log import log_error, log_warning
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from agno.db.base import BaseDb
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
def get_sort_value(record: Dict[str, Any], sort_by: str) -> Any:
|
|
@@ -138,3 +142,35 @@ def deserialize_session_json_fields(session: dict) -> dict:
|
|
|
138
142
|
log_warning(f"Warning: Could not parse runs as JSON, keeping as string: {e}")
|
|
139
143
|
|
|
140
144
|
return session
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def db_from_dict(db_data: Dict[str, Any]) -> Optional[Union["BaseDb"]]:
|
|
148
|
+
"""
|
|
149
|
+
Create a database instance from a dictionary.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
db_data: Dictionary containing database configuration
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Database instance or None if creation fails
|
|
156
|
+
"""
|
|
157
|
+
db_type = db_data.get("type")
|
|
158
|
+
if db_type == "postgres":
|
|
159
|
+
try:
|
|
160
|
+
from agno.db.postgres import PostgresDb
|
|
161
|
+
|
|
162
|
+
return PostgresDb.from_dict(db_data)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
log_error(f"Error reconstructing PostgresDb from dictionary: {e}")
|
|
165
|
+
return None
|
|
166
|
+
elif db_type == "sqlite":
|
|
167
|
+
try:
|
|
168
|
+
from agno.db.sqlite import SqliteDb
|
|
169
|
+
|
|
170
|
+
return SqliteDb.from_dict(db_data)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
log_error(f"Error reconstructing SqliteDb from dictionary: {e}")
|
|
173
|
+
return None
|
|
174
|
+
else:
|
|
175
|
+
log_warning(f"Unknown database type: {db_type}")
|
|
176
|
+
return None
|
|
@@ -7,7 +7,7 @@ import requests
|
|
|
7
7
|
from agno.agent.agent import Agent, RunOutput
|
|
8
8
|
from agno.media import Audio, File, Image, Video
|
|
9
9
|
from agno.team.team import Team, TeamRunOutput
|
|
10
|
-
from agno.utils.log import log_info, log_warning
|
|
10
|
+
from agno.utils.log import log_error, log_info, log_warning
|
|
11
11
|
from agno.utils.message import get_text_from_message
|
|
12
12
|
|
|
13
13
|
try:
|
|
@@ -126,6 +126,11 @@ class DiscordClient:
|
|
|
126
126
|
audio=[Audio(url=message_audio)] if message_audio else None,
|
|
127
127
|
files=[File(content=message_file)] if message_file else None,
|
|
128
128
|
)
|
|
129
|
+
if agent_response.status == "ERROR":
|
|
130
|
+
log_error(agent_response.content)
|
|
131
|
+
agent_response.content = (
|
|
132
|
+
"Sorry, there was an error processing your message. Please try again later."
|
|
133
|
+
)
|
|
129
134
|
await self._handle_response_in_thread(agent_response, thread)
|
|
130
135
|
elif self.team:
|
|
131
136
|
self.team.additional_context = additional_context
|
|
@@ -138,6 +143,12 @@ class DiscordClient:
|
|
|
138
143
|
audio=[Audio(url=message_audio)] if message_audio else None,
|
|
139
144
|
files=[File(content=message_file)] if message_file else None,
|
|
140
145
|
)
|
|
146
|
+
if team_response.status == "ERROR":
|
|
147
|
+
log_error(team_response.content)
|
|
148
|
+
team_response.content = (
|
|
149
|
+
"Sorry, there was an error processing your message. Please try again later."
|
|
150
|
+
)
|
|
151
|
+
|
|
141
152
|
await self._handle_response_in_thread(team_response, thread)
|
|
142
153
|
|
|
143
154
|
async def handle_hitl(
|
agno/knowledge/__init__.py
CHANGED
agno/knowledge/chunking/code.py
CHANGED
|
@@ -6,7 +6,7 @@ try:
|
|
|
6
6
|
except ImportError:
|
|
7
7
|
raise ImportError(
|
|
8
8
|
"`chonkie` is required for code chunking. "
|
|
9
|
-
|
|
9
|
+
'Please install it using `pip install "chonkie[code]"` to use CodeChunking.'
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
from agno.knowledge.chunking.strategy import ChunkingStrategy
|
|
@@ -11,7 +11,7 @@ try:
|
|
|
11
11
|
except ImportError:
|
|
12
12
|
raise ImportError(
|
|
13
13
|
"`chonkie` is required for semantic chunking. "
|
|
14
|
-
|
|
14
|
+
'Please install it using `pip install "chonkie[semantic]"` to use SemanticChunking.'
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from agno.knowledge.chunking.strategy import ChunkingStrategy
|
|
@@ -12,6 +12,10 @@ class ChunkingStrategy(ABC):
|
|
|
12
12
|
def chunk(self, document: Document) -> List[Document]:
|
|
13
13
|
raise NotImplementedError
|
|
14
14
|
|
|
15
|
+
async def achunk(self, document: Document) -> List[Document]:
|
|
16
|
+
"""Async version of chunk. Override for truly async implementations."""
|
|
17
|
+
return self.chunk(document)
|
|
18
|
+
|
|
15
19
|
def clean_text(self, text: str) -> str:
|
|
16
20
|
"""Clean the text by replacing multiple newlines with a single newline"""
|
|
17
21
|
import re
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FileSystem Knowledge
|
|
3
|
+
====================
|
|
4
|
+
A Knowledge implementation that allows retrieval from files in a local directory.
|
|
5
|
+
|
|
6
|
+
Implements the KnowledgeProtocol and provides three tools:
|
|
7
|
+
- grep_file: Search for patterns in file contents
|
|
8
|
+
- list_files: List files matching a glob pattern
|
|
9
|
+
- get_file: Read the full contents of a specific file
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from os import walk as os_walk
|
|
14
|
+
from os.path import isabs as path_isabs
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from re import IGNORECASE
|
|
17
|
+
from re import compile as re_compile
|
|
18
|
+
from re import error as re_error
|
|
19
|
+
from re import escape as re_escape
|
|
20
|
+
from typing import Any, List, Optional
|
|
21
|
+
|
|
22
|
+
from agno.knowledge.document import Document
|
|
23
|
+
from agno.utils.log import log_debug, log_warning
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FileSystemKnowledge:
|
|
28
|
+
"""Knowledge implementation that searches files in a local directory.
|
|
29
|
+
|
|
30
|
+
Implements the KnowledgeProtocol and provides three tools to agents:
|
|
31
|
+
- grep_file(query): Search for patterns in file contents
|
|
32
|
+
- list_files(pattern): List files matching a glob pattern
|
|
33
|
+
- get_file(path): Read the full contents of a specific file
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
from agno.agent import Agent
|
|
38
|
+
from agno.knowledge.filesystem import FileSystemKnowledge
|
|
39
|
+
from agno.models.openai import OpenAIChat
|
|
40
|
+
|
|
41
|
+
# Create knowledge for a directory
|
|
42
|
+
fs_knowledge = FileSystemKnowledge(base_dir="/path/to/code")
|
|
43
|
+
|
|
44
|
+
# Agent automatically gets grep_file, list_files, get_file tools
|
|
45
|
+
agent = Agent(
|
|
46
|
+
model=OpenAIChat(id="gpt-4o"),
|
|
47
|
+
knowledge=fs_knowledge,
|
|
48
|
+
search_knowledge=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Agent can now search, list, and read files
|
|
52
|
+
agent.print_response("Find where main() is defined")
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
base_dir: str
|
|
57
|
+
max_results: int = 50
|
|
58
|
+
include_patterns: List[str] = field(default_factory=list)
|
|
59
|
+
exclude_patterns: List[str] = field(
|
|
60
|
+
default_factory=lambda: [".git", "__pycache__", "node_modules", ".venv", "venv"]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __post_init__(self):
|
|
64
|
+
self.base_path = Path(self.base_dir).resolve()
|
|
65
|
+
if not self.base_path.exists():
|
|
66
|
+
raise ValueError(f"Directory does not exist: {self.base_dir}")
|
|
67
|
+
if not self.base_path.is_dir():
|
|
68
|
+
raise ValueError(f"Path is not a directory: {self.base_dir}")
|
|
69
|
+
|
|
70
|
+
def _should_include_file(self, file_path: Path) -> bool:
|
|
71
|
+
"""Check if a file should be included based on patterns."""
|
|
72
|
+
path_str = str(file_path)
|
|
73
|
+
|
|
74
|
+
# Check exclude patterns
|
|
75
|
+
for pattern in self.exclude_patterns:
|
|
76
|
+
if pattern in path_str:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Check include patterns (if specified)
|
|
80
|
+
if self.include_patterns:
|
|
81
|
+
import fnmatch
|
|
82
|
+
|
|
83
|
+
for pattern in self.include_patterns:
|
|
84
|
+
if fnmatch.fnmatch(file_path.name, pattern):
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def _list_files(self, query: str, max_results: Optional[int] = None) -> List[Document]:
|
|
91
|
+
"""List files matching the query pattern (glob-style)."""
|
|
92
|
+
import fnmatch
|
|
93
|
+
|
|
94
|
+
results: List[Document] = []
|
|
95
|
+
limit = max_results or self.max_results
|
|
96
|
+
|
|
97
|
+
for root, dirs, files in os_walk(self.base_path):
|
|
98
|
+
# Filter out excluded directories
|
|
99
|
+
dirs[:] = [d for d in dirs if not any(excl in d for excl in self.exclude_patterns)]
|
|
100
|
+
|
|
101
|
+
for filename in files:
|
|
102
|
+
if len(results) >= limit:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
file_path = Path(root) / filename
|
|
106
|
+
if not self._should_include_file(file_path):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
rel_path = file_path.relative_to(self.base_path)
|
|
110
|
+
|
|
111
|
+
# Match against query pattern (check both filename and relative path)
|
|
112
|
+
if query and query != "*":
|
|
113
|
+
if not (fnmatch.fnmatch(filename, query) or fnmatch.fnmatch(str(rel_path), query)):
|
|
114
|
+
continue
|
|
115
|
+
results.append(
|
|
116
|
+
Document(
|
|
117
|
+
name=str(rel_path),
|
|
118
|
+
content=str(rel_path),
|
|
119
|
+
meta_data={
|
|
120
|
+
"type": "file_listing",
|
|
121
|
+
"absolute_path": str(file_path),
|
|
122
|
+
"extension": file_path.suffix,
|
|
123
|
+
"size": file_path.stat().st_size,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if len(results) >= limit:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
log_debug(f"Found {len(results)} files matching pattern: {query}")
|
|
132
|
+
return results
|
|
133
|
+
|
|
134
|
+
def _get_file(self, query: str) -> List[Document]:
|
|
135
|
+
"""Get the contents of a specific file."""
|
|
136
|
+
# Handle both relative and absolute paths
|
|
137
|
+
if path_isabs(query):
|
|
138
|
+
file_path = Path(query)
|
|
139
|
+
else:
|
|
140
|
+
file_path = self.base_path / query
|
|
141
|
+
|
|
142
|
+
if not file_path.exists():
|
|
143
|
+
log_warning(f"File not found: {query}")
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
if not file_path.is_file():
|
|
147
|
+
log_warning(f"Path is not a file: {query}")
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
152
|
+
rel_path = file_path.relative_to(self.base_path) if file_path.is_relative_to(self.base_path) else file_path
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
Document(
|
|
156
|
+
name=str(rel_path),
|
|
157
|
+
content=content,
|
|
158
|
+
meta_data={
|
|
159
|
+
"type": "file_content",
|
|
160
|
+
"absolute_path": str(file_path),
|
|
161
|
+
"extension": file_path.suffix,
|
|
162
|
+
"size": len(content),
|
|
163
|
+
"lines": content.count("\n") + 1,
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
]
|
|
167
|
+
except Exception as e:
|
|
168
|
+
log_warning(f"Error reading file {query}: {e}")
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
def _grep(self, query: str, max_results: Optional[int] = None) -> List[Document]:
|
|
172
|
+
"""Search for a pattern within file contents."""
|
|
173
|
+
results: List[Document] = []
|
|
174
|
+
limit = max_results or self.max_results
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
pattern = re_compile(query, IGNORECASE)
|
|
178
|
+
except re_error:
|
|
179
|
+
# If not a valid regex, treat as literal string
|
|
180
|
+
pattern = re_compile(re_escape(query), IGNORECASE)
|
|
181
|
+
|
|
182
|
+
for root, dirs, files in os_walk(self.base_path):
|
|
183
|
+
# Filter out excluded directories
|
|
184
|
+
dirs[:] = [d for d in dirs if not any(excl in d for excl in self.exclude_patterns)]
|
|
185
|
+
|
|
186
|
+
for filename in files:
|
|
187
|
+
if len(results) >= limit:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
file_path = Path(root) / filename
|
|
191
|
+
if not self._should_include_file(file_path):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
196
|
+
matches = list(pattern.finditer(content))
|
|
197
|
+
|
|
198
|
+
if matches:
|
|
199
|
+
# Extract matching lines with context
|
|
200
|
+
lines = content.split("\n")
|
|
201
|
+
matching_lines: List[dict[str, Any]] = []
|
|
202
|
+
|
|
203
|
+
for match in matches[:10]: # Limit matches per file
|
|
204
|
+
# Find the line number
|
|
205
|
+
line_start = content.count("\n", 0, match.start())
|
|
206
|
+
line_num = line_start + 1
|
|
207
|
+
|
|
208
|
+
# Get context (1 line before and after)
|
|
209
|
+
start_idx = max(0, line_start - 1)
|
|
210
|
+
end_idx = min(len(lines), line_start + 2)
|
|
211
|
+
context_lines = lines[start_idx:end_idx]
|
|
212
|
+
|
|
213
|
+
matching_lines.append(
|
|
214
|
+
{
|
|
215
|
+
"line": line_num,
|
|
216
|
+
"match": match.group(),
|
|
217
|
+
"context": "\n".join(context_lines),
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
rel_path = file_path.relative_to(self.base_path)
|
|
222
|
+
results.append(
|
|
223
|
+
Document(
|
|
224
|
+
name=str(rel_path),
|
|
225
|
+
content="\n---\n".join(str(m["context"]) for m in matching_lines),
|
|
226
|
+
meta_data={
|
|
227
|
+
"type": "grep_result",
|
|
228
|
+
"absolute_path": str(file_path),
|
|
229
|
+
"match_count": len(matches),
|
|
230
|
+
"matches": matching_lines[:5], # Include first 5 match details
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
# Skip files that can't be read (binary, permissions, etc.)
|
|
237
|
+
log_debug(f"Skipping file {file_path}: {e}")
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
if len(results) >= limit:
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
log_debug(f"Found {len(results)} files with matches for: {query}")
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
# ========================================================================
|
|
247
|
+
# Protocol Implementation (build_context, get_tools, retrieve)
|
|
248
|
+
# ========================================================================
|
|
249
|
+
|
|
250
|
+
def build_context(self, **kwargs) -> str:
|
|
251
|
+
"""Build context string for the agent's system prompt.
|
|
252
|
+
|
|
253
|
+
Returns instructions about the three available filesystem tools.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
**kwargs: Additional context (unused).
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Context string describing available tools.
|
|
260
|
+
"""
|
|
261
|
+
from textwrap import dedent
|
|
262
|
+
|
|
263
|
+
return dedent(
|
|
264
|
+
f"""
|
|
265
|
+
You have access to a filesystem knowledge base containing documents at: {self.base_dir}
|
|
266
|
+
|
|
267
|
+
IMPORTANT: You MUST use these tools to search and read files before answering questions.
|
|
268
|
+
Do NOT answer from your own knowledge - always search the files first.
|
|
269
|
+
|
|
270
|
+
Available tools:
|
|
271
|
+
- grep_file(query): Search for keywords or patterns in file contents. Use this to find relevant information.
|
|
272
|
+
- list_files(pattern): List available files. Use "*" to see all files, or "*.md" for specific types.
|
|
273
|
+
- get_file(path): Read the full contents of a specific file.
|
|
274
|
+
|
|
275
|
+
When answering questions:
|
|
276
|
+
1. First use grep_file to search for relevant terms in the documents
|
|
277
|
+
2. Or use list_files to see what documents are available, then get_file to read them
|
|
278
|
+
3. Base your answer on what you find in the files
|
|
279
|
+
"""
|
|
280
|
+
).strip()
|
|
281
|
+
|
|
282
|
+
def get_tools(self, **kwargs) -> List[Any]:
|
|
283
|
+
"""Get tools to expose to the agent.
|
|
284
|
+
|
|
285
|
+
Returns three filesystem tools: grep_file, list_files, get_file.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
**kwargs: Additional context (unused).
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List of filesystem tools.
|
|
292
|
+
"""
|
|
293
|
+
return [
|
|
294
|
+
self._create_grep_tool(),
|
|
295
|
+
self._create_list_files_tool(),
|
|
296
|
+
self._create_get_file_tool(),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
async def aget_tools(self, **kwargs) -> List[Any]:
|
|
300
|
+
"""Async version of get_tools."""
|
|
301
|
+
return self.get_tools(**kwargs)
|
|
302
|
+
|
|
303
|
+
def _create_grep_tool(self) -> Any:
|
|
304
|
+
"""Create the grep_file tool."""
|
|
305
|
+
from agno.tools.function import Function
|
|
306
|
+
|
|
307
|
+
def grep_file(query: str, max_results: int = 20) -> str:
|
|
308
|
+
"""Search the knowledge base files for a keyword or pattern.
|
|
309
|
+
|
|
310
|
+
Use this tool to find information in the documents. Search for relevant
|
|
311
|
+
terms from the user's question to find answers.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
query: The keyword or pattern to search for (e.g., "coffee", "cappuccino", "brewing").
|
|
315
|
+
max_results: Maximum number of files to return (default: 20).
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Matching content from files with context around each match.
|
|
319
|
+
"""
|
|
320
|
+
docs = self._grep(query, max_results=max_results)
|
|
321
|
+
|
|
322
|
+
if not docs:
|
|
323
|
+
return f"No matches found for: {query}"
|
|
324
|
+
|
|
325
|
+
results = []
|
|
326
|
+
for doc in docs:
|
|
327
|
+
results.append(f"### {doc.name}\n{doc.content}")
|
|
328
|
+
|
|
329
|
+
return "\n\n".join(results)
|
|
330
|
+
|
|
331
|
+
return Function.from_callable(grep_file, name="grep_file")
|
|
332
|
+
|
|
333
|
+
def _create_list_files_tool(self) -> Any:
|
|
334
|
+
"""Create the list_files tool."""
|
|
335
|
+
from agno.tools.function import Function
|
|
336
|
+
|
|
337
|
+
def list_files(pattern: str = "*", max_results: int = 50) -> str:
|
|
338
|
+
"""List available files in the knowledge base.
|
|
339
|
+
|
|
340
|
+
Use this to see what documents are available to search.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
pattern: Glob pattern to match (e.g., "*.md", "*.txt"). Default: "*" for all files.
|
|
344
|
+
max_results: Maximum number of files to return (default: 50).
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List of available file paths.
|
|
348
|
+
"""
|
|
349
|
+
docs = self._list_files(pattern, max_results=max_results)
|
|
350
|
+
|
|
351
|
+
if not docs:
|
|
352
|
+
return f"No files found matching: {pattern}"
|
|
353
|
+
|
|
354
|
+
file_list = [doc.name for doc in docs]
|
|
355
|
+
return f"Found {len(file_list)} files:\n" + "\n".join(f"- {f}" for f in file_list)
|
|
356
|
+
|
|
357
|
+
return Function.from_callable(list_files, name="list_files")
|
|
358
|
+
|
|
359
|
+
def _create_get_file_tool(self) -> Any:
|
|
360
|
+
"""Create the get_file tool."""
|
|
361
|
+
from agno.tools.function import Function
|
|
362
|
+
|
|
363
|
+
def get_file(path: str) -> str:
|
|
364
|
+
"""Read the full contents of a document from the knowledge base.
|
|
365
|
+
|
|
366
|
+
Use this after list_files to read a specific document.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
path: Path to the file (e.g., "coffee.md", "guide.txt").
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
The full file contents.
|
|
373
|
+
"""
|
|
374
|
+
docs = self._get_file(path)
|
|
375
|
+
|
|
376
|
+
if not docs:
|
|
377
|
+
return f"File not found: {path}"
|
|
378
|
+
|
|
379
|
+
doc = docs[0]
|
|
380
|
+
return f"### {doc.name}\n```\n{doc.content}\n```"
|
|
381
|
+
|
|
382
|
+
return Function.from_callable(get_file, name="get_file")
|
|
383
|
+
|
|
384
|
+
def retrieve(
|
|
385
|
+
self,
|
|
386
|
+
query: str,
|
|
387
|
+
max_results: Optional[int] = None,
|
|
388
|
+
**kwargs,
|
|
389
|
+
) -> List[Document]:
|
|
390
|
+
"""Retrieve documents for context injection.
|
|
391
|
+
|
|
392
|
+
Uses grep as the default retrieval method since it's most likely
|
|
393
|
+
to return relevant results for a natural language query.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
query: The query string.
|
|
397
|
+
max_results: Maximum number of results.
|
|
398
|
+
**kwargs: Additional parameters.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of Document objects.
|
|
402
|
+
"""
|
|
403
|
+
return self._grep(query, max_results=max_results or 10)
|
|
404
|
+
|
|
405
|
+
async def aretrieve(
|
|
406
|
+
self,
|
|
407
|
+
query: str,
|
|
408
|
+
max_results: Optional[int] = None,
|
|
409
|
+
**kwargs,
|
|
410
|
+
) -> List[Document]:
|
|
411
|
+
"""Async version of retrieve."""
|
|
412
|
+
return self.retrieve(query, max_results=max_results, **kwargs)
|