agno 1.8.0__py3-none-any.whl → 1.8.2__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/team/team.py CHANGED
@@ -157,6 +157,8 @@ class Team:
157
157
  add_datetime_to_instructions: bool = False
158
158
  # If True, add the current location to the instructions to give the team a sense of location
159
159
  add_location_to_instructions: bool = False
160
+ # Allows for custom timezone for datetime instructions following the TZ Database format (e.g. "Etc/UTC")
161
+ timezone_identifier: Optional[str] = None
160
162
  # If True, add the tools available to team members to the system message
161
163
  add_member_tools_to_system_message: bool = True
162
164
 
@@ -328,6 +330,7 @@ class Team:
328
330
  markdown: bool = False,
329
331
  add_datetime_to_instructions: bool = False,
330
332
  add_location_to_instructions: bool = False,
333
+ timezone_identifier: Optional[str] = None,
331
334
  add_member_tools_to_system_message: bool = True,
332
335
  system_message: Optional[Union[str, Callable, Message]] = None,
333
336
  system_message_role: str = "system",
@@ -411,6 +414,7 @@ class Team:
411
414
  self.markdown = markdown
412
415
  self.add_datetime_to_instructions = add_datetime_to_instructions
413
416
  self.add_location_to_instructions = add_location_to_instructions
417
+ self.timezone_identifier = timezone_identifier
414
418
  self.add_member_tools_to_system_message = add_member_tools_to_system_message
415
419
  self.system_message = system_message
416
420
  self.system_message_role = system_message_role
@@ -5308,7 +5312,19 @@ class Team:
5308
5312
  if self.add_datetime_to_instructions:
5309
5313
  from datetime import datetime
5310
5314
 
5311
- additional_information.append(f"The current time is {datetime.now()}")
5315
+ tz = None
5316
+
5317
+ if self.timezone_identifier:
5318
+ try:
5319
+ from zoneinfo import ZoneInfo
5320
+
5321
+ tz = ZoneInfo(self.timezone_identifier)
5322
+ except Exception:
5323
+ log_warning("Invalid timezone identifier")
5324
+
5325
+ time = datetime.now(tz) if tz else datetime.now()
5326
+
5327
+ additional_information.append(f"The current time is {time}.")
5312
5328
 
5313
5329
  # 1.3.3 Add the current location
5314
5330
  if self.add_location_to_instructions:
agno/tools/confluence.py CHANGED
@@ -2,6 +2,8 @@ import json
2
2
  from os import getenv
3
3
  from typing import Any, List, Optional
4
4
 
5
+ import requests
6
+
5
7
  from agno.tools import Toolkit
6
8
  from agno.utils.log import log_info, logger
7
9
 
@@ -55,14 +57,22 @@ class ConfluenceTools(Toolkit):
55
57
  if not self.password:
56
58
  raise ValueError("Confluence API KEY or password not provided")
57
59
 
58
- self.confluence = Confluence(
59
- url=self.url, username=self.username, password=self.password, verify_ssl=verify_ssl
60
- )
60
+ session = requests.Session()
61
+ session.verify = verify_ssl
62
+
61
63
  if not verify_ssl:
62
64
  import urllib3
63
65
 
64
66
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
65
67
 
68
+ self.confluence = Confluence(
69
+ url=self.url,
70
+ username=self.username,
71
+ password=self.password,
72
+ verify_ssl=verify_ssl,
73
+ session=session,
74
+ )
75
+
66
76
  tools: List[Any] = []
67
77
  tools.append(self.get_page_content)
68
78
  tools.append(self.get_space_key)
@@ -87,6 +97,9 @@ class ConfluenceTools(Toolkit):
87
97
  try:
88
98
  log_info(f"Retrieving page content from space '{space_name}'")
89
99
  key = self.get_space_key(space_name=space_name)
100
+ if key == "No space found":
101
+ return json.dumps({"error": f"Space '{space_name}' not found"})
102
+
90
103
  page = self.confluence.get_page_by_title(key, page_title, expand=expand)
91
104
  if page:
92
105
  log_info(f"Successfully retrieved page '{page_title}' from space '{space_name}'")
@@ -106,7 +119,20 @@ class ConfluenceTools(Toolkit):
106
119
  str: List of space details as a string.
107
120
  """
108
121
  log_info("Retrieving details for all Confluence spaces")
109
- results = self.confluence.get_all_spaces()["results"]
122
+ results = []
123
+ start = 0
124
+ limit = 50
125
+
126
+ while True:
127
+ spaces_data = self.confluence.get_all_spaces(start=start, limit=limit)
128
+ if not spaces_data.get("results"):
129
+ break
130
+ results.extend(spaces_data["results"])
131
+
132
+ if len(spaces_data["results"]) < limit:
133
+ break
134
+ start += limit
135
+
110
136
  return str(results)
111
137
 
112
138
  def get_space_key(self, space_name: str):
@@ -118,13 +144,29 @@ class ConfluenceTools(Toolkit):
118
144
  Returns:
119
145
  str: Space key or "No space found" if space doesn't exist.
120
146
  """
121
- result = self.confluence.get_all_spaces()
122
- spaces = result["results"]
147
+ start = 0
148
+ limit = 50
149
+
150
+ while True:
151
+ result = self.confluence.get_all_spaces(start=start, limit=limit)
152
+ if not result.get("results"):
153
+ break
154
+
155
+ spaces = result["results"]
156
+
157
+ for space in spaces:
158
+ if space["name"].lower() == space_name.lower():
159
+ log_info(f"Found space key for '{space_name}': {space['key']}")
160
+ return space["key"]
161
+
162
+ for space in spaces:
163
+ if space["key"] == space_name:
164
+ log_info(f"'{space_name}' is already a space key")
165
+ return space_name
123
166
 
124
- for space in spaces:
125
- if space["name"] == space_name:
126
- log_info(f"Found space key for '{space_name}'")
127
- return space["key"]
167
+ if len(spaces) < limit:
168
+ break
169
+ start += limit
128
170
 
129
171
  logger.warning(f"No space named {space_name} found")
130
172
  return "No space found"
@@ -140,9 +182,17 @@ class ConfluenceTools(Toolkit):
140
182
  """
141
183
  log_info(f"Retrieving all pages from space '{space_name}'")
142
184
  space_key = self.get_space_key(space_name)
185
+
186
+ if space_key == "No space found":
187
+ return json.dumps({"error": f"Space '{space_name}' not found"})
188
+
143
189
  page_details = self.confluence.get_all_pages_from_space(
144
190
  space_key, status=None, expand=None, content_type="page"
145
191
  )
192
+
193
+ if not page_details:
194
+ return json.dumps({"error": f"No pages found in space '{space_name}'"})
195
+
146
196
  page_details = str([{"id": page["id"], "title": page["title"]} for page in page_details])
147
197
  return page_details
148
198
 
@@ -160,6 +210,9 @@ class ConfluenceTools(Toolkit):
160
210
  """
161
211
  try:
162
212
  space_key = self.get_space_key(space_name=space_name)
213
+ if space_key == "No space found":
214
+ return json.dumps({"error": f"Space '{space_name}' not found"})
215
+
163
216
  page = self.confluence.create_page(space_key, title, body, parent_id=parent_id)
164
217
  log_info(f"Page created: {title} with ID {page['id']}")
165
218
  return json.dumps({"id": page["id"], "title": title})
agno/tools/e2b.py CHANGED
@@ -58,7 +58,7 @@ class E2BTools(Toolkit):
58
58
 
59
59
  # According to official docs, the parameter is 'timeout' (in seconds), not 'timeout_ms'
60
60
  try:
61
- self.sandbox = Sandbox(api_key=self.api_key, timeout=timeout, **self.sandbox_options)
61
+ self.sandbox = Sandbox.create(api_key=self.api_key, timeout=timeout, **self.sandbox_options)
62
62
  except Exception as e:
63
63
  logger.error(f"Warning: Could not create sandbox: {e}")
64
64
  raise e
agno/tools/firecrawl.py CHANGED
@@ -6,7 +6,8 @@ from agno.tools import Toolkit
6
6
  from agno.utils.log import logger
7
7
 
8
8
  try:
9
- from firecrawl import FirecrawlApp, ScrapeOptions # type: ignore[attr-defined]
9
+ from firecrawl import FirecrawlApp # type: ignore[attr-defined]
10
+ from firecrawl.types import ScrapeOptions
10
11
  except ImportError:
11
12
  raise ImportError("`firecrawl-py` not installed. Please install using `pip install firecrawl-py`")
12
13
 
@@ -87,7 +88,7 @@ class FirecrawlTools(Toolkit):
87
88
  if self.formats:
88
89
  params["formats"] = self.formats
89
90
 
90
- scrape_result = self.app.scrape_url(url, **params)
91
+ scrape_result = self.app.scrape(url, **params)
91
92
  return json.dumps(scrape_result.model_dump(), cls=CustomJSONEncoder)
92
93
 
93
94
  def crawl_website(self, url: str, limit: Optional[int] = None) -> str:
@@ -108,7 +109,7 @@ class FirecrawlTools(Toolkit):
108
109
 
109
110
  params["poll_interval"] = self.poll_interval
110
111
 
111
- crawl_result = self.app.crawl_url(url, **params)
112
+ crawl_result = self.app.crawl(url, **params)
112
113
  return json.dumps(crawl_result.model_dump(), cls=CustomJSONEncoder)
113
114
 
114
115
  def map_website(self, url: str) -> str:
@@ -118,7 +119,7 @@ class FirecrawlTools(Toolkit):
118
119
  url (str): The URL to map.
119
120
 
120
121
  """
121
- map_result = self.app.map_url(url)
122
+ map_result = self.app.map(url)
122
123
  return json.dumps(map_result.model_dump(), cls=CustomJSONEncoder)
123
124
 
124
125
  def search(self, query: str, limit: Optional[int] = None):
agno/tools/gmail.py CHANGED
@@ -133,7 +133,7 @@ class GmailTools(Toolkit):
133
133
  send_email (bool): Enable sending emails. Defaults to True.
134
134
  search_emails (bool): Enable searching emails. Defaults to True.
135
135
  send_email_reply (bool): Enable sending email replies. Defaults to True.
136
- creds (Optional[Credentials]): Pre-existing credentials. Defaults to None.
136
+ creds (Optional[Credentials]): Pre-fetched OAuth credentials. Use this to skip a new auth flow. Defaults to None.
137
137
  credentials_path (Optional[str]): Path to credentials file. Defaults to None.
138
138
  token_path (Optional[str]): Path to token file. Defaults to None.
139
139
  scopes (Optional[List[str]]): Custom OAuth scopes. If None, uses DEFAULT_SCOPES.
agno/tools/linear.py CHANGED
@@ -217,7 +217,7 @@ class LinearTools(Toolkit):
217
217
  """
218
218
 
219
219
  query = """
220
- mutation IssueCreate ($title: String!, $description: String!, $teamId: String!, $projectId: String!, $assigneeId: String!){
220
+ mutation IssueCreate ($title: String!, $description: String!, $teamId: String!, $projectId: String, $assigneeId: String){
221
221
  issueCreate(
222
222
  input: { title: $title, description: $description, teamId: $teamId, projectId: $projectId, assigneeId: $assigneeId}
223
223
  ) {
agno/tools/neo4j.py ADDED
@@ -0,0 +1,132 @@
1
+ import os
2
+ from typing import Any, List, Optional
3
+
4
+ try:
5
+ from neo4j import GraphDatabase
6
+ except ImportError:
7
+ raise ImportError("`neo4j` not installed. Please install using `pip install neo4j`")
8
+
9
+ from agno.tools import Toolkit
10
+ from agno.utils.log import log_debug, logger
11
+
12
+
13
+ class Neo4jTools(Toolkit):
14
+ def __init__(
15
+ self,
16
+ uri: Optional[str] = None,
17
+ user: Optional[str] = None,
18
+ password: Optional[str] = None,
19
+ database: Optional[str] = None,
20
+ list_labels: bool = True,
21
+ list_relationships: bool = True,
22
+ get_schema: bool = True,
23
+ run_cypher: bool = True,
24
+ **kwargs,
25
+ ):
26
+ """
27
+ Initialize the Neo4jTools toolkit.
28
+ Connection parameters (uri/user/password or host/port) can be provided.
29
+ If not provided, falls back to NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD env vars.
30
+
31
+ Args:
32
+ uri (Optional[str]): The Neo4j URI.
33
+ user (Optional[str]): The Neo4j username.
34
+ password (Optional[str]): The Neo4j password.
35
+ host (Optional[str]): The Neo4j host.
36
+ port (Optional[int]): The Neo4j port.
37
+ database (Optional[str]): The Neo4j database.
38
+ list_labels (bool): Whether to list node labels.
39
+ list_relationships (bool): Whether to list relationship types.
40
+ get_schema (bool): Whether to get the schema.
41
+ run_cypher (bool): Whether to run Cypher queries.
42
+ **kwargs: Additional keyword arguments.
43
+ """
44
+ # Determine the connection URI and credentials
45
+ uri = uri or os.getenv("NEO4J_URI", "bolt://localhost:7687")
46
+ user = user or os.getenv("NEO4J_USERNAME")
47
+ password = password or os.getenv("NEO4J_PASSWORD")
48
+
49
+ if user is None or password is None:
50
+ raise ValueError("Username or password for Neo4j not provided")
51
+
52
+ # Create the Neo4j driver
53
+ try:
54
+ self.driver = GraphDatabase.driver(uri, auth=(user, password)) # type: ignore
55
+ self.driver.verify_connectivity()
56
+ log_debug("Connected to Neo4j database")
57
+ except Exception as e:
58
+ logger.error(f"Failed to connect to Neo4j: {e}")
59
+ raise
60
+
61
+ self.database = database or "neo4j"
62
+
63
+ # Register toolkit methods as tools
64
+ tools: List[Any] = []
65
+ if list_labels:
66
+ tools.append(self.list_labels)
67
+ if list_relationships:
68
+ tools.append(self.list_relationship_types)
69
+ if get_schema:
70
+ tools.append(self.get_schema)
71
+ if run_cypher:
72
+ tools.append(self.run_cypher_query)
73
+ super().__init__(name="neo4j_tools", tools=tools, **kwargs)
74
+
75
+ def list_labels(self) -> list:
76
+ """
77
+ Retrieve all node labels present in the connected Neo4j database.
78
+ """
79
+ try:
80
+ log_debug("Listing node labels in Neo4j database")
81
+ with self.driver.session(database=self.database) as session:
82
+ result = session.run("CALL db.labels()")
83
+ labels = [record["label"] for record in result]
84
+ return labels
85
+ except Exception as e:
86
+ logger.error(f"Error listing labels: {e}")
87
+ return []
88
+
89
+ def list_relationship_types(self) -> list:
90
+ """
91
+ Retrieve all relationship types present in the connected Neo4j database.
92
+ """
93
+ try:
94
+ log_debug("Listing relationship types in Neo4j database")
95
+ with self.driver.session(database=self.database) as session:
96
+ result = session.run("CALL db.relationshipTypes()")
97
+ types = [record["relationshipType"] for record in result]
98
+ return types
99
+ except Exception as e:
100
+ logger.error(f"Error listing relationship types: {e}")
101
+ return []
102
+
103
+ def get_schema(self) -> list:
104
+ """
105
+ Retrieve a visualization of the database schema, including nodes and relationships.
106
+ """
107
+ try:
108
+ log_debug("Retrieving Neo4j schema visualization")
109
+ with self.driver.session(database=self.database) as session:
110
+ result = session.run("CALL db.schema.visualization()")
111
+ schema_data = result.data()
112
+ return schema_data
113
+ except Exception as e:
114
+ logger.error(f"Error getting Neo4j schema: {e}")
115
+ return []
116
+
117
+ def run_cypher_query(self, query: str) -> list:
118
+ """
119
+ Execute an arbitrary Cypher query against the connected Neo4j database.
120
+
121
+ Args:
122
+ query (str): The Cypher query string to execute.
123
+ """
124
+ try:
125
+ log_debug(f"Running Cypher query: {query}")
126
+ with self.driver.session(database=self.database) as session:
127
+ result = session.run(query) # type: ignore[arg-type]
128
+ data = result.data()
129
+ return data
130
+ except Exception as e:
131
+ logger.error(f"Error running Cypher query: {e}")
132
+ return []
agno/utils/gemini.py CHANGED
@@ -175,7 +175,37 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
175
175
 
176
176
  elif schema_type == "array" and "items" in schema_dict:
177
177
  items = convert_schema(schema_dict["items"], root_schema)
178
- return Schema(type=Type.ARRAY, description=description, items=items)
178
+ min_items = schema_dict.get("minItems")
179
+ max_items = schema_dict.get("maxItems")
180
+ return Schema(
181
+ type=Type.ARRAY,
182
+ description=description,
183
+ items=items,
184
+ min_items=min_items,
185
+ max_items=max_items,
186
+ )
187
+
188
+ elif schema_type == "string":
189
+ schema_kwargs = {
190
+ "type": Type.STRING,
191
+ "description": description,
192
+ "default": default,
193
+ }
194
+ if "format" in schema_dict:
195
+ schema_kwargs["format"] = schema_dict["format"]
196
+ return Schema(**schema_kwargs)
197
+
198
+ elif schema_type in ("integer", "number"):
199
+ schema_kwargs = {
200
+ "type": schema_type.upper(),
201
+ "description": description,
202
+ "default": default,
203
+ }
204
+ if "maximum" in schema_dict:
205
+ schema_kwargs["maximum"] = schema_dict["maximum"]
206
+ if "minimum" in schema_dict:
207
+ schema_kwargs["minimum"] = schema_dict["minimum"]
208
+ return Schema(**schema_kwargs)
179
209
 
180
210
  elif schema_type == "" and "anyOf" in schema_dict:
181
211
  any_of = []
agno/utils/location.py CHANGED
@@ -10,10 +10,10 @@ def get_location() -> Dict[str, Any]:
10
10
  try:
11
11
  response = requests.get("https://api.ipify.org?format=json", timeout=5)
12
12
  ip = response.json()["ip"]
13
- response = requests.get(f"https://ipapi.co/{ip}/json/", timeout=5)
13
+ response = requests.get(f"http://ip-api.com/json/{ip}", timeout=5)
14
14
  if response.status_code == 200:
15
15
  data = response.json()
16
- return {"city": data.get("city"), "region": data.get("region"), "country": data.get("country_name")}
16
+ return {"city": data.get("city"), "region": data.get("region"), "country": data.get("country")}
17
17
  except Exception as e:
18
18
  log_warning(f"Failed to get location: {e}")
19
19
  return {}
@@ -284,3 +284,52 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
284
284
 
285
285
  chat_messages.append({"role": ROLE_MAP[message.role], "content": content}) # type: ignore
286
286
  return chat_messages, " ".join(system_messages)
287
+
288
+
289
+ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
290
+ """
291
+ Transforms function definitions into a format accepted by the Anthropic API.
292
+ """
293
+ if not tools:
294
+ return None
295
+
296
+ parsed_tools: List[Dict[str, Any]] = []
297
+ for tool_def in tools:
298
+ if tool_def.get("type", "") != "function":
299
+ parsed_tools.append(tool_def)
300
+ continue
301
+
302
+ func_def = tool_def.get("function", {})
303
+ parameters: Dict[str, Any] = func_def.get("parameters", {})
304
+ properties: Dict[str, Any] = parameters.get("properties", {})
305
+ required: List[str] = parameters.get("required", [])
306
+ required_params: List[str] = required
307
+
308
+ if not required_params:
309
+ for param_name, param_info in properties.items():
310
+ param_type = param_info.get("type", "")
311
+ param_type_list: List[str] = [param_type] if isinstance(param_type, str) else param_type or []
312
+
313
+ if "null" not in param_type_list:
314
+ required_params.append(param_name)
315
+
316
+ input_properties: Dict[str, Any] = {}
317
+ for param_name, param_info in properties.items():
318
+ # Preserve the complete schema structure for complex types
319
+ input_properties[param_name] = param_info.copy()
320
+
321
+ # Ensure description is present (default to empty if missing)
322
+ if "description" not in input_properties[param_name]:
323
+ input_properties[param_name]["description"] = ""
324
+
325
+ tool = {
326
+ "name": func_def.get("name") or "",
327
+ "description": func_def.get("description") or "",
328
+ "input_schema": {
329
+ "type": parameters.get("type", "object"),
330
+ "properties": input_properties,
331
+ "required": required_params,
332
+ },
333
+ }
334
+ parsed_tools.append(tool)
335
+ return parsed_tools
@@ -704,3 +704,25 @@ class Qdrant(VectorDb):
704
704
 
705
705
  def delete(self) -> bool:
706
706
  return self.client.delete_collection(collection_name=self.collection)
707
+
708
+ def close(self) -> None:
709
+ """Close the Qdrant client connections."""
710
+ if self._client is not None:
711
+ try:
712
+ self._client.close()
713
+ log_debug("Qdrant client closed successfully")
714
+ except Exception as e:
715
+ log_debug(f"Error closing Qdrant client: {e}")
716
+ finally:
717
+ self._client = None
718
+
719
+ async def async_close(self) -> None:
720
+ """Close the Qdrant client connections asynchronously."""
721
+ if self._async_client is not None:
722
+ try:
723
+ await self._async_client.close()
724
+ log_debug("Async Qdrant client closed successfully")
725
+ except Exception as e:
726
+ log_debug(f"Error closing async Qdrant client: {e}")
727
+ finally:
728
+ self._async_client = None
@@ -3273,7 +3273,8 @@ class Workflow:
3273
3273
  """Update executor with workflow_session_state"""
3274
3274
  if self.workflow_session_state is not None:
3275
3275
  # Update session_state with workflow_session_state
3276
- executor.workflow_session_state = self.workflow_session_state
3276
+ if hasattr(executor, "workflow_session_state"):
3277
+ executor.workflow_session_state = self.workflow_session_state
3277
3278
 
3278
3279
  def _save_run_to_storage(self, workflow_run_response: WorkflowRunResponse) -> None:
3279
3280
  """Helper method to save workflow run response to storage"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 1.8.0
3
+ Version: 1.8.2
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  License: Copyright (c) Agno, Inc.
@@ -553,6 +553,8 @@ Provides-Extra: oxylabs
553
553
  Requires-Dist: oxylabs; extra == "oxylabs"
554
554
  Provides-Extra: trafilatura
555
555
  Requires-Dist: trafilatura; extra == "trafilatura"
556
+ Provides-Extra: neo4j
557
+ Requires-Dist: neo4j; extra == "neo4j"
556
558
  Provides-Extra: sql
557
559
  Requires-Dist: sqlalchemy; extra == "sql"
558
560
  Provides-Extra: postgres
@@ -670,6 +672,7 @@ Requires-Dist: agno[memori]; extra == "tools"
670
672
  Requires-Dist: agno[google_bigquery]; extra == "tools"
671
673
  Requires-Dist: agno[psycopg]; extra == "tools"
672
674
  Requires-Dist: agno[trafilatura]; extra == "tools"
675
+ Requires-Dist: agno[neo4j]; extra == "tools"
673
676
  Provides-Extra: storage
674
677
  Requires-Dist: agno[sql]; extra == "storage"
675
678
  Requires-Dist: agno[postgres]; extra == "storage"