agno 1.7.12__py3-none-any.whl → 1.8.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/agent.py CHANGED
@@ -352,6 +352,7 @@ class Agent:
352
352
  agent_id: Optional[str] = None,
353
353
  introduction: Optional[str] = None,
354
354
  user_id: Optional[str] = None,
355
+ app_id: Optional[str] = None,
355
356
  session_id: Optional[str] = None,
356
357
  session_name: Optional[str] = None,
357
358
  session_state: Optional[Dict[str, Any]] = None,
@@ -443,6 +444,7 @@ class Agent:
443
444
  self.agent_id = agent_id
444
445
  self.introduction = introduction
445
446
  self.user_id = user_id
447
+ self.app_id = app_id
446
448
 
447
449
  self.session_id = session_id
448
450
  self.session_name = session_name
@@ -747,9 +749,7 @@ class Agent:
747
749
  self.session_id = session_id = str(uuid4())
748
750
 
749
751
  # Use the default user_id when necessary
750
- if user_id is not None and user_id != "":
751
- user_id = user_id
752
- else:
752
+ if user_id is None or user_id == "":
753
753
  user_id = self.user_id
754
754
 
755
755
  # Determine the session_state
agno/app/agui/utils.py CHANGED
@@ -129,7 +129,7 @@ def _create_events_from_chunk(
129
129
  Process a single chunk and return events to emit + updated message_started state.
130
130
  Returns: (events_to_emit, new_message_started_state)
131
131
  """
132
- events_to_emit = []
132
+ events_to_emit: List[BaseEvent] = []
133
133
 
134
134
  # Extract content if the contextual event is a content event
135
135
  if chunk.event == RunEvent.run_response_content:
@@ -13,7 +13,7 @@ from agno.media import Audio, Image, Video
13
13
  from agno.media import File as FileMedia
14
14
  from agno.run.response import RunResponseErrorEvent
15
15
  from agno.run.team import RunResponseErrorEvent as TeamRunResponseErrorEvent
16
- from agno.run.team import TeamRunResponseEvent
16
+ from agno.run.team import TeamRunResponse, TeamRunResponseEvent
17
17
  from agno.run.v2.workflow import WorkflowErrorEvent
18
18
  from agno.team.team import Team
19
19
  from agno.utils.log import logger
@@ -425,15 +425,18 @@ def get_async_router(
425
425
  )
426
426
  return run_response.to_dict()
427
427
  elif team:
428
- team_run_response = await team.arun(
429
- message=message,
430
- session_id=session_id,
431
- user_id=user_id,
432
- images=base64_images if base64_images else None,
433
- audio=base64_audios if base64_audios else None,
434
- videos=base64_videos if base64_videos else None,
435
- files=document_files if document_files else None,
436
- stream=False,
428
+ team_run_response = cast(
429
+ TeamRunResponse,
430
+ await team.arun(
431
+ message=message,
432
+ session_id=session_id,
433
+ user_id=user_id,
434
+ images=base64_images if base64_images else None,
435
+ audio=base64_audios if base64_audios else None,
436
+ videos=base64_videos if base64_videos else None,
437
+ files=document_files if document_files else None,
438
+ stream=False,
439
+ ),
437
440
  )
438
441
  return team_run_response.to_dict()
439
442
  elif workflow:
agno/knowledge/agent.py CHANGED
@@ -161,7 +161,8 @@ class AgentKnowledge(BaseModel):
161
161
 
162
162
  # Upsert documents if upsert is True and vector db supports upsert
163
163
  if upsert and self.vector_db.upsert_available():
164
- self.vector_db.upsert(documents=documents_to_load, filters=doc.meta_data)
164
+ for doc in documents_to_load:
165
+ self.vector_db.upsert(documents=[doc], filters=doc.meta_data)
165
166
  # Insert documents
166
167
  else:
167
168
  # Filter out documents which already exist in the vector db
@@ -170,7 +171,8 @@ class AgentKnowledge(BaseModel):
170
171
  documents_to_load = self.filter_existing_documents(document_list)
171
172
 
172
173
  if documents_to_load:
173
- self.vector_db.insert(documents=documents_to_load, filters=doc.meta_data)
174
+ for doc in documents_to_load:
175
+ self.vector_db.insert(documents=[doc], filters=doc.meta_data)
174
176
 
175
177
  num_documents += len(documents_to_load)
176
178
  log_info(f"Added {num_documents} documents to knowledge base")
@@ -204,7 +206,8 @@ class AgentKnowledge(BaseModel):
204
206
 
205
207
  # Upsert documents if upsert is True and vector db supports upsert
206
208
  if upsert and self.vector_db.upsert_available():
207
- await self.vector_db.async_upsert(documents=documents_to_load, filters=doc.meta_data)
209
+ for doc in documents_to_load:
210
+ await self.vector_db.async_upsert(documents=[doc], filters=doc.meta_data)
208
211
  # Insert documents
209
212
  else:
210
213
  # Filter out documents which already exist in the vector db
@@ -213,7 +216,8 @@ class AgentKnowledge(BaseModel):
213
216
  documents_to_load = await self.async_filter_existing_documents(document_list)
214
217
 
215
218
  if documents_to_load:
216
- await self.vector_db.async_insert(documents=documents_to_load, filters=doc.meta_data)
219
+ for doc in documents_to_load:
220
+ await self.vector_db.async_insert(documents=[doc], filters=doc.meta_data)
217
221
 
218
222
  num_documents += len(documents_to_load)
219
223
  log_info(f"Added {num_documents} documents to knowledge base")
agno/knowledge/gcs/pdf.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import AsyncIterator, Iterator, List, Optional, Dict, Any
1
+ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
2
2
 
3
3
  from agno.document import Document
4
4
  from agno.document.reader.gcs.pdf_reader import GCSPDFReader
@@ -93,7 +93,7 @@ class GCSPDFKnowledgeBase(GCSKnowledgeBase):
93
93
  document_iterator = self.async_document_lists
94
94
  async for document_list in document_iterator: # type: ignore
95
95
  documents_to_load = document_list
96
-
96
+
97
97
  # Track metadata for filtering capabilities and collect metadata for filters
98
98
  filters_metadata: Optional[Dict[str, Any]] = None
99
99
  for doc in document_list:
agno/media.py CHANGED
@@ -38,13 +38,34 @@ class ImageArtifact(Media):
38
38
  mime_type: Optional[str] = None
39
39
  alt_text: Optional[str] = None
40
40
 
41
+ def _normalise_content(self) -> Optional[Union[str, bytes]]:
42
+ if self.content is None:
43
+ return None
44
+ content_normalised: Union[str, bytes] = self.content
45
+ if content_normalised and isinstance(content_normalised, bytes):
46
+ from base64 import b64encode
47
+
48
+ try:
49
+ # First try to decode as UTF-8
50
+ content_normalised = content_normalised.decode("utf-8") # type: ignore
51
+ except UnicodeDecodeError:
52
+ # Fallback to base64 encoding for binary content
53
+ content_normalised = b64encode(bytes(content_normalised)).decode("utf-8") # type: ignore
54
+ except Exception:
55
+ # Last resort: try to convert to base64
56
+ try:
57
+ content_normalised = b64encode(bytes(content_normalised)).decode("utf-8") # type: ignore
58
+ except Exception:
59
+ pass
60
+ return content_normalised
61
+
41
62
  def to_dict(self) -> Dict[str, Any]:
63
+ content_normalised = self._normalise_content()
64
+
42
65
  response_dict = {
43
66
  "id": self.id,
44
67
  "url": self.url,
45
- "content": self.content.decode("utf-8")
46
- if self.content and isinstance(self.content, bytes)
47
- else self.content,
68
+ "content": content_normalised,
48
69
  "mime_type": self.mime_type,
49
70
  "alt_text": self.alt_text,
50
71
  }
@@ -136,7 +157,7 @@ class Video(BaseModel):
136
157
 
137
158
  @classmethod
138
159
  def from_artifact(cls, artifact: VideoArtifact) -> "Video":
139
- return cls(url=artifact.url)
160
+ return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
140
161
 
141
162
 
142
163
  class Audio(BaseModel):
@@ -308,7 +329,7 @@ class Image(BaseModel):
308
329
 
309
330
  @classmethod
310
331
  def from_artifact(cls, artifact: ImageArtifact) -> "Image":
311
- return cls(url=artifact.url)
332
+ return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
312
333
 
313
334
 
314
335
  class File(BaseModel):
@@ -19,7 +19,7 @@ class DashScope(OpenAILike):
19
19
  provider (str): The provider name. Defaults to "Qwen".
20
20
  api_key (Optional[str]): The DashScope API key.
21
21
  base_url (str): The base URL. Defaults to "https://dashscope-intl.aliyuncs.com/compatible-mode/v1".
22
- enable_thinking (Optional[bool]): Enable thinking process (DashScope native parameter). Defaults to None.
22
+ enable_thinking (bool): Enable thinking process (DashScope native parameter). Defaults to False.
23
23
  include_thoughts (Optional[bool]): Include thinking process in response (alternative parameter). Defaults to None.
24
24
  """
25
25
 
@@ -31,8 +31,9 @@ class DashScope(OpenAILike):
31
31
  base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
32
32
 
33
33
  # Thinking parameters
34
- enable_thinking: Optional[bool] = None
34
+ enable_thinking: bool = False
35
35
  include_thoughts: Optional[bool] = None
36
+ thinking_budget: Optional[int] = None
36
37
 
37
38
  # DashScope supports structured outputs
38
39
  supports_native_structured_outputs: bool = True
@@ -75,7 +76,15 @@ class DashScope(OpenAILike):
75
76
  ) -> Dict[str, Any]:
76
77
  params = super().get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice)
77
78
 
78
- should_include_thoughts = self.enable_thinking or self.include_thoughts
79
- if should_include_thoughts:
80
- params["extra_body"] = {"enable_thinking": True}
79
+ if self.include_thoughts is not None:
80
+ self.enable_thinking = self.include_thoughts
81
+
82
+ if self.enable_thinking is not None:
83
+ params["extra_body"] = {
84
+ "enable_thinking": self.enable_thinking,
85
+ }
86
+
87
+ if self.thinking_budget is not None:
88
+ params["extra_body"]["thinking_budget"] = self.thinking_budget
89
+
81
90
  return params
@@ -30,9 +30,11 @@ try:
30
30
  GoogleSearch,
31
31
  GoogleSearchRetrieval,
32
32
  Part,
33
+ Retrieval,
33
34
  ThinkingConfig,
34
35
  Tool,
35
36
  UrlContext,
37
+ VertexAISearch,
36
38
  )
37
39
  from google.genai.types import (
38
40
  File as GeminiFile,
@@ -70,6 +72,8 @@ class Gemini(Model):
70
72
  grounding: bool = False
71
73
  grounding_dynamic_threshold: Optional[float] = None
72
74
  url_context: bool = False
75
+ vertexai_search: bool = False
76
+ vertexai_search_datastore: Optional[str] = None
73
77
 
74
78
  temperature: Optional[float] = None
75
79
  top_p: Optional[float] = None
@@ -204,7 +208,9 @@ class Gemini(Model):
204
208
  builtin_tools = []
205
209
 
206
210
  if self.grounding:
207
- log_info("Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead.")
211
+ log_info(
212
+ "Grounding enabled. This is a legacy tool. For Gemini 2.0+ Please use enable `search` flag instead."
213
+ )
208
214
  builtin_tools.append(
209
215
  Tool(
210
216
  google_search=GoogleSearchRetrieval(
@@ -223,6 +229,15 @@ class Gemini(Model):
223
229
  log_info("URL context enabled.")
224
230
  builtin_tools.append(Tool(url_context=UrlContext()))
225
231
 
232
+ if self.vertexai_search:
233
+ log_info("Vertex AI Search enabled.")
234
+ if not self.vertexai_search_datastore:
235
+ log_error("vertexai_search_datastore must be provided when vertexai_search is enabled.")
236
+ raise ValueError("vertexai_search_datastore must be provided when vertexai_search is enabled.")
237
+ builtin_tools.append(
238
+ Tool(retrieval=Retrieval(vertex_ai_search=VertexAISearch(datastore=self.vertexai_search_datastore)))
239
+ )
240
+
226
241
  # Set tools in config
227
242
  if builtin_tools:
228
243
  if tools:
@@ -778,12 +793,18 @@ class Gemini(Model):
778
793
  grounding_metadata = response.candidates[0].grounding_metadata.model_dump()
779
794
  citations_raw["grounding_metadata"] = grounding_metadata
780
795
 
781
- chunks = grounding_metadata.get("grounding_chunks", [])
782
- citation_pairs = [
783
- (chunk.get("web", {}).get("uri"), chunk.get("web", {}).get("title"))
784
- for chunk in chunks
785
- if chunk.get("web", {}).get("uri")
786
- ]
796
+ chunks = grounding_metadata.get("grounding_chunks", []) or []
797
+ citation_pairs = []
798
+ for chunk in chunks:
799
+ if not isinstance(chunk, dict):
800
+ continue
801
+ web = chunk.get("web")
802
+ if not isinstance(web, dict):
803
+ continue
804
+ uri = web.get("uri")
805
+ title = web.get("title")
806
+ if uri:
807
+ citation_pairs.append((uri, title))
787
808
 
788
809
  # Create citation objects from filtered pairs
789
810
  grounding_urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
@@ -892,11 +913,17 @@ class Gemini(Model):
892
913
 
893
914
  # Extract url and title
894
915
  chunks = grounding_metadata.pop("grounding_chunks", None) or []
895
- citation_pairs = [
896
- (chunk.get("web", {}).get("uri"), chunk.get("web", {}).get("title"))
897
- for chunk in chunks
898
- if chunk.get("web", {}).get("uri")
899
- ]
916
+ citation_pairs = []
917
+ for chunk in chunks:
918
+ if not isinstance(chunk, dict):
919
+ continue
920
+ web = chunk.get("web")
921
+ if not isinstance(web, dict):
922
+ continue
923
+ uri = web.get("uri")
924
+ title = web.get("title")
925
+ if uri:
926
+ citation_pairs.append((uri, title))
900
927
 
901
928
  # Create citation objects from filtered pairs
902
929
  citations.urls = [UrlCitation(url=url, title=title) for url, title in citation_pairs]
@@ -77,7 +77,7 @@ class OpenAIChat(Model):
77
77
  max_retries: Optional[int] = None
78
78
  default_headers: Optional[Any] = None
79
79
  default_query: Optional[Any] = None
80
- http_client: Optional[httpx.Client] = None
80
+ http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
81
81
  client_params: Optional[Dict[str, Any]] = None
82
82
 
83
83
  # The role to map the message role to.
@@ -123,8 +123,11 @@ class OpenAIChat(Model):
123
123
  OpenAIClient: An instance of the OpenAI client.
124
124
  """
125
125
  client_params: Dict[str, Any] = self._get_client_params()
126
- if self.http_client is not None:
127
- client_params["http_client"] = self.http_client
126
+ if self.http_client:
127
+ if isinstance(self.http_client, httpx.Client):
128
+ client_params["http_client"] = self.http_client
129
+ else:
130
+ log_warning("http_client is not an instance of httpx.Client.")
128
131
  return OpenAIClient(**client_params)
129
132
 
130
133
  def get_async_client(self) -> AsyncOpenAIClient:
@@ -136,7 +139,14 @@ class OpenAIChat(Model):
136
139
  """
137
140
  client_params: Dict[str, Any] = self._get_client_params()
138
141
  if self.http_client:
139
- client_params["http_client"] = self.http_client
142
+ if isinstance(self.http_client, httpx.AsyncClient):
143
+ client_params["http_client"] = self.http_client
144
+ else:
145
+ log_warning("http_client is not an instance of httpx.AsyncClient. Using default httpx.AsyncClient.")
146
+ # Create a new async HTTP client with custom limits
147
+ client_params["http_client"] = httpx.AsyncClient(
148
+ limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
149
+ )
140
150
  else:
141
151
  # Create a new async HTTP client with custom limits
142
152
  client_params["http_client"] = httpx.AsyncClient(
@@ -44,6 +44,7 @@ class OpenAIResponses(Model):
44
44
  reasoning: Optional[Dict[str, Any]] = None
45
45
  verbosity: Optional[Literal["low", "medium", "high"]] = None
46
46
  reasoning_effort: Optional[Literal["minimal", "medium", "high"]] = None
47
+ reasoning_summary: Optional[Literal["auto", "concise", "detailed"]] = None
47
48
  store: Optional[bool] = None
48
49
  temperature: Optional[float] = None
49
50
  top_p: Optional[float] = None
@@ -84,6 +85,18 @@ class OpenAIResponses(Model):
84
85
  """Return True if the contextual used model is a known reasoning model."""
85
86
  return self.id.startswith("o3") or self.id.startswith("o4-mini") or self.id.startswith("gpt-5")
86
87
 
88
+ def _set_reasoning_request_param(self, base_params: Dict[str, Any]) -> Dict[str, Any]:
89
+ """Set the reasoning request parameter."""
90
+ base_params["reasoning"] = self.reasoning or {}
91
+
92
+ if self.reasoning_effort is not None:
93
+ base_params["reasoning"]["effort"] = self.reasoning_effort
94
+
95
+ if self.reasoning_summary is not None:
96
+ base_params["reasoning"]["summary"] = self.reasoning_summary
97
+
98
+ return base_params
99
+
87
100
  def _get_client_params(self) -> Dict[str, Any]:
88
101
  """
89
102
  Get client parameters for API requests.
@@ -185,12 +198,8 @@ class OpenAIResponses(Model):
185
198
  "user": self.user,
186
199
  "service_tier": self.service_tier,
187
200
  }
188
-
189
- # Handle reasoning parameter - convert reasoning_effort to reasoning format
190
- if self.reasoning is not None:
191
- base_params["reasoning"] = self.reasoning
192
- elif self.reasoning_effort is not None:
193
- base_params["reasoning"] = {"effort": self.reasoning_effort}
201
+ # Populate the reasoning parameter
202
+ base_params = self._set_reasoning_request_param(base_params)
194
203
 
195
204
  # Build text parameter
196
205
  text_params: Dict[str, Any] = {}
@@ -478,7 +487,6 @@ class OpenAIResponses(Model):
478
487
  request_params = self.get_request_params(
479
488
  messages=messages, response_format=response_format, tools=tools, tool_choice=tool_choice
480
489
  )
481
-
482
490
  return self.get_client().responses.create(
483
491
  model=self.id,
484
492
  input=self._format_messages(messages), # type: ignore
@@ -730,7 +738,10 @@ class OpenAIResponses(Model):
730
738
 
731
739
  # Add role
732
740
  model_response.role = "assistant"
741
+ reasoning_summary: str = ""
742
+
733
743
  for output in response.output:
744
+ # Add content
734
745
  if output.type == "message":
735
746
  model_response.content = response.output_text
736
747
 
@@ -746,6 +757,8 @@ class OpenAIResponses(Model):
746
757
  citations.urls.append(UrlCitation(url=annotation.url, title=annotation.title))
747
758
  if citations.urls or citations.documents:
748
759
  model_response.citations = citations
760
+
761
+ # Add tool calls
749
762
  elif output.type == "function_call":
750
763
  if model_response.tool_calls is None:
751
764
  model_response.tool_calls = []
@@ -765,10 +778,24 @@ class OpenAIResponses(Model):
765
778
  model_response.extra = model_response.extra or {}
766
779
  model_response.extra.setdefault("tool_call_ids", []).append(output.call_id)
767
780
 
768
- # i.e. we asked for reasoning, so we need to add the reasoning content
769
- if self.reasoning is not None:
781
+ # Add reasoning summary
782
+ elif output.type == "reasoning":
783
+ if reasoning_summaries := getattr(output, "summary", None):
784
+ for summary in reasoning_summaries:
785
+ if isinstance(summary, dict):
786
+ summary_text = summary.get("text")
787
+ else:
788
+ summary_text = getattr(summary, "text", None)
789
+ if summary_text:
790
+ reasoning_summary = (reasoning_summary or "") + summary_text
791
+
792
+ # Add reasoning content
793
+ if reasoning_summary is not None:
794
+ model_response.reasoning_content = reasoning_summary
795
+ elif self.reasoning is not None:
770
796
  model_response.reasoning_content = response.output_text
771
797
 
798
+ # Add metrics
772
799
  if response.usage is not None:
773
800
  model_response.response_usage = response.usage
774
801
 
@@ -835,7 +862,8 @@ class OpenAIResponses(Model):
835
862
  model_response.content = stream_event.delta
836
863
  stream_data.response_content += stream_event.delta
837
864
 
838
- if self.reasoning is not None:
865
+ # Treat the output_text deltas as reasoning content if the reasoning summary is not requested.
866
+ if self.reasoning is not None and self.reasoning_summary is None:
839
867
  model_response.reasoning_content = stream_event.delta
840
868
  stream_data.response_thinking += stream_event.delta
841
869
 
@@ -868,7 +896,24 @@ class OpenAIResponses(Model):
868
896
 
869
897
  elif stream_event.type == "response.completed":
870
898
  model_response = ModelResponse()
871
- # Add usage metrics if present
899
+
900
+ # Add reasoning summary
901
+ if self.reasoning_summary is not None:
902
+ summary_text: str = ""
903
+ for out in getattr(stream_event.response, "output", []) or []:
904
+ if getattr(out, "type", None) == "reasoning":
905
+ summaries = getattr(out, "summary", None)
906
+ if summaries:
907
+ for s in summaries:
908
+ text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
909
+ if text_val:
910
+ if summary_text:
911
+ summary_text += "\n\n"
912
+ summary_text += text_val
913
+ if summary_text:
914
+ model_response.reasoning_content = summary_text
915
+
916
+ # Add metrics
872
917
  if stream_event.response.usage is not None:
873
918
  model_response.response_usage = stream_event.response.usage
874
919
 
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})