Skip to content

Commit 856753a

Browse files
authored
Merge branch 'main' into release-2.5.3
2 parents fc4b27d + b65efee commit 856753a

File tree

14 files changed

+628
-17
lines changed

14 files changed

+628
-17
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from agno.agent import Agent
2+
from agno.db.sqlite import SqliteDb
3+
from agno.models.openai import OpenAIChat
4+
from agno.tools.file_generation import FileGenerationTools
5+
from agno.workflow.step import Step
6+
from agno.workflow.workflow import Workflow
7+
8+
# ---------------------------------------------------------------------------
9+
# Step 1: Generate a Report
10+
# ---------------------------------------------------------------------------
11+
report_generator = Agent(
12+
name="Report Generator",
13+
model=OpenAIChat(id="gpt-4o-mini"),
14+
tools=[FileGenerationTools(enable_pdf_generation=True)],
15+
instructions=[
16+
"You are a data analyst that generates reports.",
17+
"When asked to create a report, use the generate_pdf_file tool to create it.",
18+
"Include meaningful data in the report.",
19+
],
20+
)
21+
22+
generate_report_step = Step(
23+
name="Generate Report",
24+
agent=report_generator,
25+
description="Generate a PDF report with quarterly sales data",
26+
)
27+
28+
# ---------------------------------------------------------------------------
29+
# Step 2: Analyze the Report
30+
# ---------------------------------------------------------------------------
31+
report_analyzer = Agent(
32+
name="Report Analyzer",
33+
model=OpenAIChat(id="gpt-4o"),
34+
instructions=[
35+
"You are a business analyst.",
36+
"Analyze the attached PDF report and provide insights.",
37+
"Focus on trends, anomalies, and recommendations.",
38+
],
39+
)
40+
41+
analyze_report_step = Step(
42+
name="Analyze Report",
43+
agent=report_analyzer,
44+
description="Analyze the generated report and provide insights",
45+
)
46+
47+
# ---------------------------------------------------------------------------
48+
# Create Workflow
49+
# ---------------------------------------------------------------------------
50+
report_workflow = Workflow(
51+
name="Report Generation and Analysis",
52+
description="Generate a report and analyze it for insights",
53+
db=SqliteDb(
54+
session_table="file_propagation_workflow",
55+
db_file="tmp/file_propagation_workflow.db",
56+
),
57+
steps=[generate_report_step, analyze_report_step],
58+
)
59+
60+
# ---------------------------------------------------------------------------
61+
# Run Workflow
62+
# ---------------------------------------------------------------------------
63+
if __name__ == "__main__":
64+
print("=" * 60)
65+
print("File Generation and Propagation Workflow")
66+
print("=" * 60)
67+
print()
68+
print("This workflow demonstrates file propagation between steps:")
69+
print("1. Step 1 generates a PDF report using FileGenerationTools")
70+
print("2. The file is automatically propagated to Step 2")
71+
print("3. Step 2 analyzes the report content")
72+
print()
73+
print("-" * 60)
74+
75+
result = report_workflow.run(
76+
input="Create a quarterly sales report for Q4 2024 with data for 4 regions (North, South, East, West) and then analyze it for insights.",
77+
)
78+
79+
print()
80+
print("=" * 60)
81+
print("Workflow Result")
82+
print("=" * 60)
83+
print()
84+
print(result.content)
85+
print()
86+
87+
# Show file propagation
88+
print("-" * 60)
89+
print("Files in workflow output:")
90+
if result.files:
91+
for f in result.files:
92+
print(f" - {f.filename} ({f.mime_type}, {f.size} bytes)")
93+
else:
94+
print(" No files in final output (files were consumed by analysis step)")
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
Demonstrates isolate_vector_search combined with list-based FilterExpr filters.
3+
4+
When multiple Knowledge instances share the same vector database and
5+
isolate_vector_search=True, each instance's searches are scoped to its own data
6+
via an auto-injected linked_to filter. This works seamlessly with user-supplied
7+
FilterExpr filters — the linked_to filter is prepended automatically.
8+
9+
This cookbook shows:
10+
1. Two Knowledge instances sharing one vector database, each isolated.
11+
2. Inserting documents with metadata into each instance.
12+
3. Querying via an Agent with knowledge_filters using EQ, IN, AND, NOT operators.
13+
4. The linked_to filter is auto-injected alongside user filters.
14+
"""
15+
16+
from agno.agent import Agent
17+
from agno.filters import AND, EQ, IN, NOT
18+
from agno.knowledge.knowledge import Knowledge
19+
from agno.utils.media import (
20+
SampleDataFileExtension,
21+
download_knowledge_filters_sample_data,
22+
)
23+
from agno.vectordb.pgvector import PgVector
24+
25+
# Download sample CSV files — 4 files with sales/survey/financial data
26+
downloaded_csv_paths = download_knowledge_filters_sample_data(
27+
num_files=4, file_extension=SampleDataFileExtension.CSV
28+
)
29+
30+
# Shared vector database — both Knowledge instances use the same table
31+
vector_db = PgVector(
32+
table_name="isolated_filter_demo",
33+
db_url="postgresql+psycopg://ai:ai@localhost:5532/ai",
34+
)
35+
36+
# -----------------------------------------------------------------------------
37+
# Two isolated Knowledge instances sharing the same vector database
38+
# -----------------------------------------------------------------------------
39+
40+
sales_knowledge = Knowledge(
41+
name="sales-data",
42+
description="Sales and financial data",
43+
vector_db=vector_db,
44+
isolate_vector_search=True, # Scoped to sales-data documents only
45+
)
46+
47+
survey_knowledge = Knowledge(
48+
name="survey-data",
49+
description="Customer survey data",
50+
vector_db=vector_db,
51+
isolate_vector_search=True, # Scoped to survey-data documents only
52+
)
53+
54+
# -----------------------------------------------------------------------------
55+
# Insert documents into each isolated instance
56+
# Documents are tagged with linked_to metadata automatically
57+
# -----------------------------------------------------------------------------
58+
59+
# Sales documents go into the sales knowledge instance
60+
sales_knowledge.insert_many(
61+
[
62+
{
63+
"path": downloaded_csv_paths[0],
64+
"metadata": {
65+
"data_type": "sales",
66+
"quarter": "Q1",
67+
"year": 2024,
68+
"region": "north_america",
69+
"currency": "USD",
70+
},
71+
},
72+
{
73+
"path": downloaded_csv_paths[1],
74+
"metadata": {
75+
"data_type": "sales",
76+
"year": 2024,
77+
"region": "europe",
78+
"currency": "EUR",
79+
},
80+
},
81+
{
82+
"path": downloaded_csv_paths[3],
83+
"metadata": {
84+
"data_type": "financial",
85+
"sector": "technology",
86+
"year": 2024,
87+
"report_type": "quarterly_earnings",
88+
},
89+
},
90+
],
91+
)
92+
93+
# Survey documents go into the survey knowledge instance
94+
survey_knowledge.insert_many(
95+
[
96+
{
97+
"path": downloaded_csv_paths[2],
98+
"metadata": {
99+
"data_type": "survey",
100+
"survey_type": "customer_satisfaction",
101+
"year": 2024,
102+
"target_demographic": "mixed",
103+
},
104+
},
105+
],
106+
)
107+
108+
# -----------------------------------------------------------------------------
109+
# Query with list-based FilterExpr filters
110+
# The linked_to filter is auto-injected alongside any user-supplied filters
111+
# -----------------------------------------------------------------------------
112+
113+
sales_agent = Agent(
114+
knowledge=sales_knowledge,
115+
search_knowledge=True,
116+
)
117+
118+
survey_agent = Agent(
119+
knowledge=survey_knowledge,
120+
search_knowledge=True,
121+
)
122+
123+
# EQ filter on the sales-isolated instance
124+
# Effective filters: linked_to="sales-data" AND region="north_america"
125+
print("--- Sales agent: EQ filter (North America only) ---")
126+
sales_agent.print_response(
127+
"Describe revenue performance for the region",
128+
knowledge_filters=[EQ("region", "north_america")],
129+
markdown=True,
130+
)
131+
132+
# IN filter on the sales-isolated instance
133+
# Effective filters: linked_to="sales-data" AND region IN ["north_america", "europe"]
134+
print("--- Sales agent: IN filter (multiple regions) ---")
135+
sales_agent.print_response(
136+
"Compare revenue across regions",
137+
knowledge_filters=[IN("region", ["north_america", "europe"])],
138+
markdown=True,
139+
)
140+
141+
# AND + NOT compound filter on the sales-isolated instance
142+
# Effective filters: linked_to="sales-data" AND data_type="sales" AND NOT region="europe"
143+
print("--- Sales agent: AND + NOT compound filter ---")
144+
sales_agent.print_response(
145+
"Describe revenue performance excluding Europe",
146+
knowledge_filters=[AND(EQ("data_type", "sales"), NOT(EQ("region", "europe")))],
147+
markdown=True,
148+
)
149+
150+
# Survey agent — isolated to survey-data only, even though it shares the same vector DB
151+
# Effective filters: linked_to="survey-data" AND survey_type="customer_satisfaction"
152+
print("--- Survey agent: EQ filter (customer satisfaction) ---")
153+
survey_agent.print_response(
154+
"Summarize the customer satisfaction survey results",
155+
knowledge_filters=[EQ("survey_type", "customer_satisfaction")],
156+
markdown=True,
157+
)

libs/agno/agno/db/postgres/async_postgres.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
2626
from agno.db.schemas.knowledge import KnowledgeRow
2727
from agno.db.schemas.memory import UserMemory
28+
from agno.db.utils import json_serializer
2829
from agno.session import AgentSession, Session, TeamSession, WorkflowSession
2930
from agno.utils.log import log_debug, log_error, log_info, log_warning
3031
from agno.utils.string import sanitize_postgres_string, sanitize_postgres_strings
@@ -127,6 +128,7 @@ def __init__(
127128
db_url,
128129
pool_pre_ping=True,
129130
pool_recycle=3600,
131+
json_serializer=json_serializer,
130132
)
131133
if _engine is None:
132134
raise ValueError("One of db_url or db_engine must be provided")

libs/agno/agno/db/postgres/postgres.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
2626
from agno.db.schemas.knowledge import KnowledgeRow
2727
from agno.db.schemas.memory import UserMemory
28+
from agno.db.utils import json_serializer
2829
from agno.session import AgentSession, Session, TeamSession, WorkflowSession
2930
from agno.utils.log import log_debug, log_error, log_info, log_warning
3031
from agno.utils.string import generate_id, sanitize_postgres_string, sanitize_postgres_strings
@@ -121,6 +122,7 @@ def __init__(
121122
db_url,
122123
pool_pre_ping=True,
123124
pool_recycle=3600,
125+
json_serializer=json_serializer,
124126
)
125127
if _engine is None:
126128
raise ValueError("One of db_url or db_engine must be provided")

libs/agno/agno/db/utils.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,46 @@ def default(self, obj):
5252
return super().default(obj)
5353

5454

55+
def json_serializer(obj: Any) -> str:
56+
"""Custom JSON serializer for SQLAlchemy engine.
57+
58+
This function is used as the json_serializer parameter when creating
59+
SQLAlchemy engines for PostgreSQL. It handles non-JSON-serializable
60+
types like datetime, date, UUID, etc.
61+
62+
Args:
63+
obj: The object to serialize to JSON.
64+
65+
Returns:
66+
JSON string representation of the object.
67+
"""
68+
return json.dumps(obj, cls=CustomJSONEncoder)
69+
70+
5571
def serialize_session_json_fields(session: dict) -> dict:
5672
"""Serialize all JSON fields in the given Session dictionary.
5773
74+
Uses CustomJSONEncoder to handle non-JSON-serializable types like
75+
datetime, date, UUID, Message, Metrics, etc.
76+
5877
Args:
5978
data (dict): The dictionary to serialize JSON fields in.
6079
6180
Returns:
6281
dict: The dictionary with JSON fields serialized.
6382
"""
6483
if session.get("session_data") is not None:
65-
session["session_data"] = json.dumps(session["session_data"])
84+
session["session_data"] = json.dumps(session["session_data"], cls=CustomJSONEncoder)
6685
if session.get("agent_data") is not None:
67-
session["agent_data"] = json.dumps(session["agent_data"])
86+
session["agent_data"] = json.dumps(session["agent_data"], cls=CustomJSONEncoder)
6887
if session.get("team_data") is not None:
69-
session["team_data"] = json.dumps(session["team_data"])
88+
session["team_data"] = json.dumps(session["team_data"], cls=CustomJSONEncoder)
7089
if session.get("workflow_data") is not None:
71-
session["workflow_data"] = json.dumps(session["workflow_data"])
90+
session["workflow_data"] = json.dumps(session["workflow_data"], cls=CustomJSONEncoder)
7291
if session.get("metadata") is not None:
73-
session["metadata"] = json.dumps(session["metadata"])
92+
session["metadata"] = json.dumps(session["metadata"], cls=CustomJSONEncoder)
7493
if session.get("chat_history") is not None:
75-
session["chat_history"] = json.dumps(session["chat_history"])
94+
session["chat_history"] = json.dumps(session["chat_history"], cls=CustomJSONEncoder)
7695
if session.get("summary") is not None:
7796
session["summary"] = json.dumps(session["summary"], cls=CustomJSONEncoder)
7897
if session.get("runs") is not None:

libs/agno/agno/knowledge/knowledge.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from agno.db.base import AsyncBaseDb, BaseDb
1515
from agno.db.schemas.knowledge import KnowledgeRow
16-
from agno.filters import FilterExpr
16+
from agno.filters import EQ, FilterExpr
1717
from agno.knowledge.content import Content, ContentAuth, ContentStatus, FileData
1818
from agno.knowledge.document import Document
1919
from agno.knowledge.reader import Reader, ReaderFactory
@@ -535,7 +535,8 @@ def search(
535535
search_filters = {"linked_to": self.name}
536536
elif isinstance(search_filters, dict):
537537
search_filters = {**search_filters, "linked_to": self.name}
538-
# List-based filters: user must add linked_to filter manually
538+
elif isinstance(search_filters, list):
539+
search_filters = [EQ("linked_to", self.name), *search_filters]
539540

540541
_max_results = max_results or self.max_results
541542
log_debug(f"Getting {_max_results} relevant documents for query: {query}")
@@ -574,7 +575,8 @@ async def asearch(
574575
search_filters = {"linked_to": self.name}
575576
elif isinstance(search_filters, dict):
576577
search_filters = {**search_filters, "linked_to": self.name}
577-
# List-based filters: user must add linked_to filter manually
578+
elif isinstance(search_filters, list):
579+
search_filters = [EQ("linked_to", self.name), *search_filters]
578580

579581
_max_results = max_results or self.max_results
580582
log_debug(f"Getting {_max_results} relevant documents for query: {query}")

0 commit comments

Comments
 (0)