agno 2.4.1__py3-none-any.whl → 2.4.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.
@@ -0,0 +1,100 @@
1
+ from dataclasses import dataclass, field
2
+ from os import getenv
3
+ from typing import Any, Dict, Optional
4
+
5
+ from agno.models.openai.open_responses import OpenResponses
6
+ from agno.utils.log import log_debug
7
+
8
+
9
+ @dataclass
10
+ class OllamaResponses(OpenResponses):
11
+ """
12
+ A class for interacting with Ollama models using the OpenAI Responses API.
13
+
14
+ This uses Ollama's OpenAI-compatible `/v1/responses` endpoint, which was added
15
+ in Ollama v0.13.3. It allows using Ollama models with the Responses API format.
16
+
17
+ Note: Ollama's Responses API is stateless - it does not support `previous_response_id`
18
+ or conversation chaining. Each request is independent.
19
+
20
+ Requirements:
21
+ - Ollama v0.13.3 or later
22
+ - For local usage: Ollama server running at http://localhost:11434
23
+ - For Ollama Cloud: Set OLLAMA_API_KEY environment variable
24
+
25
+ For more information, see: https://docs.ollama.com/api/openai-compatibility
26
+
27
+ Attributes:
28
+ id (str): The model id. Defaults to "gpt-oss:20b".
29
+ name (str): The model name. Defaults to "OllamaResponses".
30
+ provider (str): The provider name. Defaults to "Ollama".
31
+ host (Optional[str]): The Ollama server host. Defaults to "http://localhost:11434".
32
+ api_key (Optional[str]): The API key for Ollama Cloud. Not required for local usage.
33
+ """
34
+
35
+ id: str = "gpt-oss:20b"
36
+ name: str = "OllamaResponses"
37
+ provider: str = "Ollama"
38
+
39
+ # Ollama server host - defaults to local instance
40
+ host: Optional[str] = None
41
+
42
+ # API key for Ollama Cloud (not required for local)
43
+ api_key: Optional[str] = field(default_factory=lambda: getenv("OLLAMA_API_KEY"))
44
+
45
+ # Ollama's Responses API is stateless
46
+ store: Optional[bool] = False
47
+
48
+ def _get_client_params(self) -> Dict[str, Any]:
49
+ """
50
+ Get client parameters for API requests.
51
+
52
+ Returns:
53
+ Dict[str, Any]: Client parameters including base_url and optional api_key.
54
+ """
55
+ # Determine the base URL
56
+ if self.host:
57
+ base_url = self.host.rstrip("/")
58
+ if not base_url.endswith("/v1"):
59
+ base_url = f"{base_url}/v1"
60
+ elif self.api_key:
61
+ # Ollama Cloud
62
+ base_url = "https://ollama.com/v1"
63
+ log_debug(f"Using Ollama Cloud endpoint: {base_url}")
64
+ else:
65
+ # Local Ollama instance
66
+ base_url = "http://localhost:11434/v1"
67
+
68
+ # Build client params
69
+ base_params: Dict[str, Any] = {
70
+ "base_url": base_url,
71
+ "timeout": self.timeout,
72
+ "max_retries": self.max_retries,
73
+ "default_headers": self.default_headers,
74
+ "default_query": self.default_query,
75
+ }
76
+
77
+ # Add API key if provided (required for Ollama Cloud, ignored for local)
78
+ if self.api_key:
79
+ base_params["api_key"] = self.api_key
80
+ else:
81
+ # OpenAI client requires an api_key, but Ollama ignores it locally
82
+ base_params["api_key"] = "ollama"
83
+
84
+ # Filter out None values
85
+ client_params = {k: v for k, v in base_params.items() if v is not None}
86
+
87
+ # Add additional client params if provided
88
+ if self.client_params:
89
+ client_params.update(self.client_params)
90
+
91
+ return client_params
92
+
93
+ def _using_reasoning_model(self) -> bool:
94
+ """
95
+ Ollama doesn't have native reasoning models like OpenAI's o-series.
96
+
97
+ Some models may support thinking/reasoning through their architecture
98
+ (like DeepSeek-R1), but they don't use OpenAI's reasoning API format.
99
+ """
100
+ return False
@@ -1,9 +1,11 @@
1
1
  from agno.models.openai.chat import OpenAIChat
2
2
  from agno.models.openai.like import OpenAILike
3
+ from agno.models.openai.open_responses import OpenResponses
3
4
  from agno.models.openai.responses import OpenAIResponses
4
5
 
5
6
  __all__ = [
6
7
  "OpenAIChat",
7
8
  "OpenAILike",
8
9
  "OpenAIResponses",
10
+ "OpenResponses",
9
11
  ]
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ from agno.models.openai.responses import OpenAIResponses
5
+
6
+
7
+ @dataclass
8
+ class OpenResponses(OpenAIResponses):
9
+ """
10
+ A base class for interacting with any provider using the Open Responses API specification.
11
+
12
+ Open Responses is an open-source specification for building multi-provider, interoperable
13
+ LLM interfaces based on the OpenAI Responses API. This class provides a foundation for
14
+ providers that implement the spec (e.g., Ollama, OpenRouter).
15
+
16
+ For more information, see: https://openresponses.org
17
+
18
+ Key differences from OpenAIResponses:
19
+ - Configurable base_url for pointing to different API endpoints
20
+ - Stateless by default (no previous_response_id chaining)
21
+ - Flexible api_key handling for providers that don't require authentication
22
+
23
+ Args:
24
+ id (str): The model id. Defaults to "not-provided".
25
+ name (str): The model name. Defaults to "OpenResponses".
26
+ api_key (Optional[str]): The API key. Defaults to "not-provided".
27
+ """
28
+
29
+ id: str = "not-provided"
30
+ name: str = "OpenResponses"
31
+ provider: str = "OpenResponses"
32
+ api_key: Optional[str] = "not-provided"
33
+
34
+ # Disable stateful features by default for compatible providers
35
+ # Most OpenAI-compatible providers don't support previous_response_id chaining
36
+ store: Optional[bool] = False
37
+
38
+ def _using_reasoning_model(self) -> bool:
39
+ """
40
+ Override to disable reasoning model detection for compatible providers.
41
+
42
+ Most compatible providers don't support OpenAI's reasoning models,
43
+ so we disable the special handling by default. Subclasses can override
44
+ this if they support specific reasoning models.
45
+ """
46
+ return False
@@ -1,5 +1,7 @@
1
1
  from agno.models.openrouter.openrouter import OpenRouter
2
+ from agno.models.openrouter.responses import OpenRouterResponses
2
3
 
3
4
  __all__ = [
4
5
  "OpenRouter",
6
+ "OpenRouterResponses",
5
7
  ]
@@ -0,0 +1,146 @@
1
+ from dataclasses import dataclass
2
+ from os import getenv
3
+ from typing import Any, Dict, List, Optional, Type, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from agno.exceptions import ModelAuthenticationError
8
+ from agno.models.openai.open_responses import OpenResponses
9
+ from agno.models.message import Message
10
+
11
+
12
+ @dataclass
13
+ class OpenRouterResponses(OpenResponses):
14
+ """
15
+ A class for interacting with OpenRouter models using the OpenAI Responses API.
16
+
17
+ OpenRouter's Responses API (currently in beta) provides OpenAI-compatible access
18
+ to multiple AI models through a unified interface. It supports tools, reasoning,
19
+ streaming, and plugins.
20
+
21
+ Note: OpenRouter's Responses API is stateless - each request is independent and
22
+ no server-side state is persisted.
23
+
24
+ For more information, see: https://openrouter.ai/docs/api/reference/responses/overview
25
+
26
+ Attributes:
27
+ id (str): The model id. Defaults to "openai/gpt-oss-20b".
28
+ name (str): The model name. Defaults to "OpenRouterResponses".
29
+ provider (str): The provider name. Defaults to "OpenRouter".
30
+ api_key (Optional[str]): The API key. Uses OPENROUTER_API_KEY env var if not set.
31
+ base_url (str): The base URL. Defaults to "https://openrouter.ai/api/v1".
32
+ models (Optional[List[str]]): List of fallback model IDs to use if the primary model
33
+ fails due to rate limits, timeouts, or unavailability. OpenRouter will automatically
34
+ try these models in order. Example: ["anthropic/claude-sonnet-4", "deepseek/deepseek-r1"]
35
+
36
+ Example:
37
+ ```python
38
+ from agno.agent import Agent
39
+ from agno.models.openrouter import OpenRouterResponses
40
+
41
+ agent = Agent(
42
+ model=OpenRouterResponses(id="anthropic/claude-sonnet-4"),
43
+ markdown=True,
44
+ )
45
+ agent.print_response("Write a haiku about coding")
46
+ ```
47
+ """
48
+
49
+ id: str = "openai/gpt-oss-20b"
50
+ name: str = "OpenRouterResponses"
51
+ provider: str = "OpenRouter"
52
+
53
+ api_key: Optional[str] = None
54
+ base_url: str = "https://openrouter.ai/api/v1"
55
+
56
+ # Dynamic model routing - fallback models if primary fails
57
+ # https://openrouter.ai/docs/features/model-routing
58
+ models: Optional[List[str]] = None
59
+
60
+ # OpenRouter's Responses API is stateless
61
+ store: Optional[bool] = False
62
+
63
+ def _get_client_params(self) -> Dict[str, Any]:
64
+ """
65
+ Returns client parameters for API requests, checking for OPENROUTER_API_KEY.
66
+
67
+ Returns:
68
+ Dict[str, Any]: A dictionary of client parameters for API requests.
69
+
70
+ Raises:
71
+ ModelAuthenticationError: If OPENROUTER_API_KEY is not set.
72
+ """
73
+ # Fetch API key from env if not already set
74
+ if not self.api_key:
75
+ self.api_key = getenv("OPENROUTER_API_KEY")
76
+ if not self.api_key:
77
+ raise ModelAuthenticationError(
78
+ message="OPENROUTER_API_KEY not set. Please set the OPENROUTER_API_KEY environment variable.",
79
+ model_name=self.name,
80
+ )
81
+
82
+ # Build client params
83
+ base_params: Dict[str, Any] = {
84
+ "api_key": self.api_key,
85
+ "base_url": self.base_url,
86
+ "organization": self.organization,
87
+ "timeout": self.timeout,
88
+ "max_retries": self.max_retries,
89
+ "default_headers": self.default_headers,
90
+ "default_query": self.default_query,
91
+ }
92
+
93
+ # Filter out None values
94
+ client_params = {k: v for k, v in base_params.items() if v is not None}
95
+
96
+ # Add additional client params if provided
97
+ if self.client_params:
98
+ client_params.update(self.client_params)
99
+
100
+ return client_params
101
+
102
+ def get_request_params(
103
+ self,
104
+ messages: Optional[List[Message]] = None,
105
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
106
+ tools: Optional[List[Dict[str, Any]]] = None,
107
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
108
+ ) -> Dict[str, Any]:
109
+ """
110
+ Returns keyword arguments for API requests, including fallback models configuration.
111
+
112
+ Returns:
113
+ Dict[str, Any]: A dictionary of keyword arguments for API requests.
114
+ """
115
+ # Get base request params from parent class
116
+ request_params = super().get_request_params(
117
+ messages=messages,
118
+ response_format=response_format,
119
+ tools=tools,
120
+ tool_choice=tool_choice,
121
+ )
122
+
123
+ # Add fallback models to extra_body if specified
124
+ if self.models:
125
+ # Get existing extra_body or create new dict
126
+ extra_body = request_params.get("extra_body") or {}
127
+
128
+ # Merge fallback models into extra_body
129
+ extra_body["models"] = self.models
130
+
131
+ # Update request params
132
+ request_params["extra_body"] = extra_body
133
+
134
+ return request_params
135
+
136
+ def _using_reasoning_model(self) -> bool:
137
+ """
138
+ Check if the model is a reasoning model that requires special handling.
139
+
140
+ OpenRouter hosts various reasoning models, but they may not all use
141
+ OpenAI's reasoning API format. We check for known reasoning model patterns.
142
+ """
143
+ # Check for OpenAI reasoning models hosted on OpenRouter
144
+ if self.id.startswith("openai/o3") or self.id.startswith("openai/o4"):
145
+ return True
146
+ return False
@@ -175,7 +175,7 @@ class RemoteContentSourceSchema(BaseModel):
175
175
 
176
176
  id: str = Field(..., description="Unique identifier for the content source")
177
177
  name: str = Field(..., description="Display name for the content source")
178
- type: str = Field(..., description="Type of content source (s3, gcs, sharepoint, github)")
178
+ type: str = Field(..., description="Type of content source (s3, gcs, sharepoint, github, azureblob)")
179
179
  metadata: Optional[Dict[str, Any]] = Field(None, description="Custom metadata for the content source")
180
180
 
181
181
 
@@ -109,7 +109,7 @@ class LightRag(VectorDb):
109
109
  async with httpx.AsyncClient(timeout=30.0) as client:
110
110
  response = await client.post(
111
111
  f"{self.server_url}/query",
112
- json={"query": query, "mode": "hybrid"},
112
+ json={"query": query, "mode": "hybrid", "include_references": True},
113
113
  headers=self._get_headers(),
114
114
  )
115
115
 
@@ -322,7 +322,7 @@ class LightRag(VectorDb):
322
322
  async with httpx.AsyncClient(timeout=30.0) as client:
323
323
  response = await client.post(
324
324
  f"{self.server_url}/query",
325
- json={"query": query, "mode": "hybrid"},
325
+ json={"query": query, "mode": "hybrid", "include_references": True},
326
326
  headers=self._get_headers(),
327
327
  )
328
328
 
@@ -349,10 +349,11 @@ class LightRag(VectorDb):
349
349
  # LightRAG server returns a dict with 'response' key, but we expect a list of documents
350
350
  # Convert the response to the expected format
351
351
  if isinstance(result, dict) and "response" in result:
352
- # Wrap the response in a Document object
353
- return [
354
- Document(content=result["response"], meta_data={"source": "lightrag", "query": query, "mode": mode})
355
- ]
352
+ meta_data = {"source": "lightrag", "query": query, "mode": mode}
353
+ # Preserve references from LightRAG response for document citations
354
+ if "references" in result:
355
+ meta_data["references"] = result["references"]
356
+ return [Document(content=result["response"], meta_data=meta_data)]
356
357
  elif isinstance(result, list):
357
358
  # Convert list items to Document objects
358
359
  documents = []
@@ -241,7 +241,7 @@ class Milvus(VectorDb):
241
241
  "id": doc_id,
242
242
  "text": cleaned_content,
243
243
  "name": document.name,
244
- "content_id": document.content_id,
244
+ "content_id": document.content_id or "",
245
245
  "meta_data": meta_data_str,
246
246
  "content": cleaned_content,
247
247
  "usage": usage_str,
@@ -334,6 +334,7 @@ class Milvus(VectorDb):
334
334
  scroll_result = self.client.query(
335
335
  collection_name=self.collection,
336
336
  filter=expr,
337
+ output_fields=["id"],
337
338
  limit=1,
338
339
  )
339
340
  return len(scroll_result) > 0 and len(scroll_result[0]) > 0
@@ -363,6 +364,7 @@ class Milvus(VectorDb):
363
364
  scroll_result = self.client.query(
364
365
  collection_name=self.collection,
365
366
  filter=expr,
367
+ output_fields=["id"],
366
368
  limit=1,
367
369
  )
368
370
  return len(scroll_result) > 0 and len(scroll_result[0]) > 0
@@ -429,7 +431,7 @@ class Milvus(VectorDb):
429
431
  "id": doc_id,
430
432
  "vector": document.embedding,
431
433
  "name": document.name,
432
- "content_id": document.content_id,
434
+ "content_id": document.content_id or "",
433
435
  "meta_data": meta_data,
434
436
  "content": cleaned_content,
435
437
  "usage": document.usage,
@@ -512,7 +514,7 @@ class Milvus(VectorDb):
512
514
  "id": doc_id,
513
515
  "vector": document.embedding,
514
516
  "name": document.name,
515
- "content_id": document.content_id,
517
+ "content_id": document.content_id or "",
516
518
  "meta_data": meta_data,
517
519
  "content": cleaned_content,
518
520
  "usage": document.usage,
@@ -547,30 +549,41 @@ class Milvus(VectorDb):
547
549
  filters (Optional[Dict[str, Any]]): Filters to apply while upserting
548
550
  """
549
551
  log_debug(f"Upserting {len(documents)} documents")
550
- for document in documents:
551
- document.embed(embedder=self.embedder)
552
- cleaned_content = document.content.replace("\x00", "\ufffd")
553
- doc_id = md5(cleaned_content.encode()).hexdigest()
554
-
555
- meta_data = document.meta_data or {}
556
- if filters:
557
- meta_data.update(filters)
558
-
559
- data = {
560
- "id": doc_id,
561
- "vector": document.embedding,
562
- "name": document.name,
563
- "content_id": document.content_id,
564
- "meta_data": document.meta_data,
565
- "content": cleaned_content,
566
- "usage": document.usage,
567
- "content_hash": content_hash,
568
- }
569
- self.client.upsert(
570
- collection_name=self.collection,
571
- data=data,
572
- )
573
- log_debug(f"Upserted document: {document.name} ({document.meta_data})")
552
+
553
+ if self.search_type == SearchType.hybrid:
554
+ for document in documents:
555
+ document.embed(embedder=self.embedder)
556
+ data = self._prepare_document_data(content_hash=content_hash, document=document, include_vectors=True)
557
+ self.client.upsert(
558
+ collection_name=self.collection,
559
+ data=data,
560
+ )
561
+ log_debug(f"Upserted hybrid document: {document.name} ({document.meta_data})")
562
+ else:
563
+ for document in documents:
564
+ document.embed(embedder=self.embedder)
565
+ cleaned_content = document.content.replace("\x00", "\ufffd")
566
+ doc_id = md5(cleaned_content.encode()).hexdigest()
567
+
568
+ meta_data = document.meta_data or {}
569
+ if filters:
570
+ meta_data.update(filters)
571
+
572
+ data = {
573
+ "id": doc_id,
574
+ "vector": document.embedding,
575
+ "name": document.name,
576
+ "content_id": document.content_id or "",
577
+ "meta_data": meta_data, # type: ignore[dict-item]
578
+ "content": cleaned_content,
579
+ "usage": document.usage, # type: ignore[dict-item]
580
+ "content_hash": content_hash,
581
+ }
582
+ self.client.upsert(
583
+ collection_name=self.collection,
584
+ data=data,
585
+ )
586
+ log_debug(f"Upserted document: {document.name} ({document.meta_data})")
574
587
 
575
588
  async def async_upsert(
576
589
  self, content_hash: str, documents: List[Document], filters: Optional[Dict[str, Any]] = None
@@ -616,28 +629,46 @@ class Milvus(VectorDb):
616
629
  embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
617
630
  await asyncio.gather(*embed_tasks, return_exceptions=True)
618
631
 
619
- async def process_document(document):
620
- cleaned_content = document.content.replace("\x00", "\ufffd")
621
- doc_id = md5(cleaned_content.encode()).hexdigest()
622
- data = {
623
- "id": doc_id,
624
- "vector": document.embedding,
625
- "name": document.name,
626
- "content_id": document.content_id,
627
- "meta_data": document.meta_data,
628
- "content": cleaned_content,
629
- "usage": document.usage,
630
- "content_hash": content_hash,
631
- }
632
- await self.async_client.upsert(
633
- collection_name=self.collection,
634
- data=data,
635
- )
636
- log_debug(f"Upserted document asynchronously: {document.name} ({document.meta_data})")
637
- return data
632
+ if self.search_type == SearchType.hybrid:
633
+
634
+ async def process_hybrid_document(document):
635
+ data = self._prepare_document_data(content_hash=content_hash, document=document, include_vectors=True)
636
+ await self.async_client.upsert(
637
+ collection_name=self.collection,
638
+ data=data,
639
+ )
640
+ log_debug(f"Upserted hybrid document asynchronously: {document.name} ({document.meta_data})")
641
+ return data
642
+
643
+ await asyncio.gather(*[process_hybrid_document(doc) for doc in documents])
644
+ else:
645
+
646
+ async def process_document(document):
647
+ cleaned_content = document.content.replace("\x00", "\ufffd")
648
+ doc_id = md5(cleaned_content.encode()).hexdigest()
649
+
650
+ meta_data = document.meta_data or {}
651
+ if filters:
652
+ meta_data.update(filters)
653
+
654
+ data = {
655
+ "id": doc_id,
656
+ "vector": document.embedding,
657
+ "name": document.name,
658
+ "content_id": document.content_id or "",
659
+ "meta_data": meta_data, # type: ignore[dict-item]
660
+ "content": cleaned_content,
661
+ "usage": document.usage, # type: ignore[dict-item]
662
+ "content_hash": content_hash,
663
+ }
664
+ await self.async_client.upsert(
665
+ collection_name=self.collection,
666
+ data=data,
667
+ )
668
+ log_debug(f"Upserted document asynchronously: {document.name} ({document.meta_data})")
669
+ return data
638
670
 
639
- # Process all documents in parallel
640
- await asyncio.gather(*[process_document(doc) for doc in documents])
671
+ await asyncio.gather(*[process_document(doc) for doc in documents])
641
672
 
642
673
  log_debug(f"Upserted {len(documents)} documents asynchronously in parallel")
643
674
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  Project-URL: homepage, https://agno.com