Compare commits

...
1 Commits
Author SHA1 Message Date
Mervin Praison 412783c5ed Release v3.2.1 2026-01-07 21:57:34 +00:00
33 changed files with 2442 additions and 385 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.2.0" \
"praisonai>=3.2.1" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.2.0" \
"praisonai>=3.2.1" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=3.2.0" \
"praisonai>=3.2.1" \
"praisonai[ui]" \
"praisonai[crewai]"
+36 -16
View File
@@ -88,17 +88,22 @@ def basic_citations():
print("BASIC CITATIONS IN RAG")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['source']}, {d['section']}]\n{d['content']}" for d in LEGAL_DOCUMENTS])
agent = Agent(
name="HR Policy Expert",
instructions="""You are an HR policy expert who answers employee questions.
instructions=f"""You are an HR policy expert who answers employee questions.
IMPORTANT: Always cite your sources using this format:
[Source: Document Name, Section]
Include the citation immediately after the relevant information.
If information comes from multiple sources, cite each one.""",
knowledge=LEGAL_DOCUMENTS,
user_id="citation_demo"
If information comes from multiple sources, cite each one.
POLICY DOCUMENTS:
{context}""",
verbose=False
)
queries = [
@@ -121,9 +126,12 @@ def structured_citations():
print("STRUCTURED CITATIONS")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['source']}, {d['section']}]\n{d['content']}" for d in LEGAL_DOCUMENTS])
agent = Agent(
name="Policy Researcher",
instructions="""You provide policy information with structured citations.
instructions=f"""You provide policy information with structured citations.
Format your response as:
@@ -134,9 +142,11 @@ def structured_citations():
1. [Document] - [Section] - [Relevant quote]
2. [Document] - [Section] - [Relevant quote]
Always include at least one source citation.""",
knowledge=LEGAL_DOCUMENTS,
user_id="structured_cite"
Always include at least one source citation.
POLICY DOCUMENTS:
{context}""",
verbose=False
)
query = "What are the requirements for remote work eligibility?"
@@ -153,9 +163,12 @@ def inline_citations():
print("INLINE CITATIONS (Academic Style)")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['source']}, {d['section']}]\n{d['content']}" for d in LEGAL_DOCUMENTS])
agent = Agent(
name="Academic Researcher",
instructions="""You write responses with inline citations like academic papers.
instructions=f"""You write responses with inline citations like academic papers.
Use numbered citations [1], [2], etc. in the text.
List full references at the end.
@@ -166,9 +179,11 @@ def inline_citations():
References:
[1] Employee Handbook v2024, Section 3.2
[2] Employee Handbook v2024, Section 4.1""",
knowledge=LEGAL_DOCUMENTS,
user_id="inline_cite"
[2] Employee Handbook v2024, Section 4.1
POLICY DOCUMENTS:
{context}""",
verbose=False
)
query = "Summarize the key employee benefits."
@@ -235,9 +250,12 @@ def multi_source_synthesis():
print("MULTI-SOURCE SYNTHESIS WITH CITATIONS")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['source']}, {d['section']}]\n{d['content']}" for d in LEGAL_DOCUMENTS])
agent = Agent(
name="Benefits Advisor",
instructions="""You synthesize information from multiple policy documents.
instructions=f"""You synthesize information from multiple policy documents.
When answering:
1. Gather relevant information from all sources
@@ -245,9 +263,11 @@ def multi_source_synthesis():
3. Cite each source for the specific information it provides
4. Note if sources have different or complementary information
Format: Include [Source: X] after each piece of information.""",
knowledge=LEGAL_DOCUMENTS,
user_id="multi_source"
Format: Include [Source: X] after each piece of information.
POLICY DOCUMENTS:
{context}""",
verbose=False
)
query = "Give me a complete overview of employee benefits and policies."
+27 -12
View File
@@ -110,17 +110,22 @@ def web_augmented_rag():
print("WEB-AUGMENTED RAG")
print("=" * 60)
# Build context from company knowledge
context = "\n\n".join([f"[{k['id']}]\n{k['content']}" for k in COMPANY_KNOWLEDGE])
# Create agent that can use web search
agent = Agent(
name="Research Assistant",
instructions="""You are a research assistant that combines internal knowledge
instructions=f"""You are a research assistant that combines internal knowledge
with web search results. When answering:
1. Check if the question requires current/real-time information
2. Use web search for news, weather, current events
3. Combine web results with your knowledge for comprehensive answers
4. Clearly indicate when information comes from web search.""",
knowledge=COMPANY_KNOWLEDGE,
user_id="web_demo"
4. Clearly indicate when information comes from web search.
COMPANY KNOWLEDGE:
{context}""",
verbose=False
)
# Simulate web-augmented queries
@@ -155,13 +160,18 @@ def database_augmented_rag():
print("DATABASE-AUGMENTED RAG")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{k['id']}]\n{k['content']}" for k in COMPANY_KNOWLEDGE])
agent = Agent(
name="Business Analyst",
instructions="""You are a business analyst with access to company databases.
instructions=f"""You are a business analyst with access to company databases.
Interpret data accurately and provide actionable insights.
When presenting numbers, add context and trends.""",
knowledge=COMPANY_KNOWLEDGE,
user_id="db_demo"
When presenting numbers, add context and trends.
COMPANY KNOWLEDGE:
{context}""",
verbose=False
)
queries = [
@@ -228,13 +238,18 @@ def hybrid_source_rag():
print("HYBRID SOURCE RAG")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{k['id']}]\n{k['content']}" for k in COMPANY_KNOWLEDGE])
agent = Agent(
name="Executive Assistant",
instructions="""You are an executive assistant with access to multiple data sources.
instructions=f"""You are an executive assistant with access to multiple data sources.
Synthesize information from static knowledge, databases, and APIs.
Provide comprehensive briefings that combine all relevant data.""",
knowledge=COMPANY_KNOWLEDGE,
user_id="hybrid_demo"
Provide comprehensive briefings that combine all relevant data.
COMPANY KNOWLEDGE:
{context}""",
verbose=False
)
query = "Give me a complete business briefing for today"
+25 -31
View File
@@ -143,19 +143,20 @@ def demonstrate_search_types():
def hybrid_rag_example():
"""Demonstrate hybrid retrieval with an agent."""
# Create agent with hybrid retrieval enabled
# Build context from troubleshooting guides
context = "\n\n".join([f"[{g['id']}]\n{g['content']}" for g in TROUBLESHOOTING_GUIDES])
# Create agent with context in instructions
agent = Agent(
name="Tech Support",
instructions="""You are a technical support specialist.
instructions=f"""You are a technical support specialist.
Help users troubleshoot errors using the knowledge base.
Always mention the specific error code when relevant.
Provide step-by-step resolution guidance.""",
knowledge=TROUBLESHOOTING_GUIDES,
knowledge_config={
"hybrid": True, # Enable hybrid search
"top_k": 3
},
user_id="hybrid_demo"
Provide step-by-step resolution guidance.
TROUBLESHOOTING KNOWLEDGE BASE:
{context}""",
verbose=False
)
# Test with different query types
@@ -191,35 +192,28 @@ def compare_retrieval_modes():
print("RETRIEVAL MODE COMPARISON")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{g['id']}]\n{g['content']}" for g in TROUBLESHOOTING_GUIDES])
query = "database transaction blocking issue"
# Dense-only agent
dense_agent = Agent(
name="Dense Search Agent",
instructions="Answer based on the knowledge provided.",
knowledge=TROUBLESHOOTING_GUIDES,
knowledge_config={"hybrid": False},
user_id="dense_demo"
)
# Hybrid agent
hybrid_agent = Agent(
name="Hybrid Search Agent",
instructions="Answer based on the knowledge provided.",
knowledge=TROUBLESHOOTING_GUIDES,
knowledge_config={"hybrid": True},
user_id="hybrid_demo"
# Agent with context
agent = Agent(
name="Search Agent",
instructions=f"""Answer based on the knowledge provided.
KNOWLEDGE BASE:
{context}""",
verbose=False
)
print(f"\n📝 Query: {query}")
print("\n🔍 Dense-Only Search:")
dense_response = dense_agent.chat(query)
print(f" {dense_response[:200]}...")
print("\n🔍 RAG Response:")
response = agent.chat(query)
print(f" {response[:200]}...")
print("\n🔍 Hybrid Search:")
hybrid_response = hybrid_agent.chat(query)
print(f" {hybrid_response[:200]}...")
print("\n💡 Note: Hybrid search combines semantic + keyword matching for best results.")
def hybrid_search_tuning():
+30 -14
View File
@@ -109,17 +109,22 @@ ENTITY_KNOWLEDGE = [
def graph_aware_rag():
"""Demonstrate relationship-aware retrieval."""
# Build context from entity knowledge
context = "\n\n".join([f"[{e['type'].upper()}: {e['id']}]\n{e['content']}" for e in ENTITY_KNOWLEDGE])
# Create agent with entity knowledge
agent = Agent(
name="Physics Historian",
instructions="""You are a physics historian who understands the relationships
instructions=f"""You are a physics historian who understands the relationships
between scientists, concepts, and institutions. When answering:
1. Trace connections between entities
2. Explain how ideas and people influenced each other
3. Reference specific relationships mentioned in the knowledge base
4. Build a narrative that shows the interconnected nature of physics history.""",
knowledge=ENTITY_KNOWLEDGE,
user_id="graph_demo"
4. Build a narrative that shows the interconnected nature of physics history.
KNOWLEDGE GRAPH:
{context}""",
verbose=False
)
# Relationship-focused queries
@@ -151,13 +156,18 @@ def multi_hop_reasoning():
print("MULTI-HOP REASONING")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{e['type'].upper()}: {e['id']}]\n{e['content']}" for e in ENTITY_KNOWLEDGE])
agent = Agent(
name="Connection Finder",
instructions="""You excel at finding indirect connections between entities.
instructions=f"""You excel at finding indirect connections between entities.
When asked about relationships, trace the path through intermediate entities.
Format your answer to show the chain of connections.""",
knowledge=ENTITY_KNOWLEDGE,
user_id="multihop_demo"
Format your answer to show the chain of connections.
KNOWLEDGE GRAPH:
{context}""",
verbose=False
)
# Multi-hop query requiring traversal
@@ -182,22 +192,28 @@ def entity_type_filtering():
# Filter to only people
people_only = [e for e in ENTITY_KNOWLEDGE if e['type'] == 'person']
people_context = "\n\n".join([f"[{e['id']}]\n{e['content']}" for e in people_only])
people_agent = Agent(
name="Biographer",
instructions="You are a biographer focused on scientists' lives and relationships.",
knowledge=people_only,
user_id="people_demo"
instructions=f"""You are a biographer focused on scientists' lives and relationships.
SCIENTISTS:
{people_context}""",
verbose=False
)
# Filter to only concepts
concepts_only = [e for e in ENTITY_KNOWLEDGE if e['type'] == 'concept']
concepts_context = "\n\n".join([f"[{e['id']}]\n{e['content']}" for e in concepts_only])
concept_agent = Agent(
name="Concept Explainer",
instructions="You explain scientific concepts and their relationships.",
knowledge=concepts_only,
user_id="concept_demo"
instructions=f"""You explain scientific concepts and their relationships.
CONCEPTS:
{concepts_context}""",
verbose=False
)
query = "Tell me about the major developments in early 20th century physics."
+28 -14
View File
@@ -99,15 +99,20 @@ ALL_DOCUMENTS = [CLIMATE_REPORT, ENERGY_REPORT, POLICY_REPORT, TECH_REPORT]
def multi_document_rag():
"""Demonstrate RAG across multiple documents."""
# Build context from all documents
context = "\n\n".join([f"[{d['source']}]\n{d['content']}" for d in ALL_DOCUMENTS])
# Create agent with multi-document knowledge
agent = Agent(
name="Climate Analyst",
instructions="""You are a climate and energy analyst.
instructions=f"""You are a climate and energy analyst.
Synthesize information from multiple reports to answer questions.
When information comes from different sources, mention which reports
you're drawing from. Provide comprehensive, well-rounded answers.""",
knowledge=ALL_DOCUMENTS,
user_id="multi_doc_demo"
you're drawing from. Provide comprehensive, well-rounded answers.
REPORTS:
{context}""",
verbose=False
)
# Questions that require multi-document synthesis
@@ -138,16 +143,21 @@ def cross_reference_example():
print("CROSS-REFERENCING ACROSS DOCUMENTS")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['source']}]\n{d['content']}" for d in ALL_DOCUMENTS])
agent = Agent(
name="Cross-Reference Analyst",
instructions="""You are an analyst who excels at finding connections
instructions=f"""You are an analyst who excels at finding connections
between different reports. When answering:
1. Identify relevant information from each source
2. Note any contradictions or complementary data
3. Synthesize a coherent narrative
4. Cite your sources explicitly.""",
knowledge=ALL_DOCUMENTS,
user_id="cross_ref_demo"
4. Cite your sources explicitly.
REPORTS:
{context}""",
verbose=False
)
query = """
@@ -171,16 +181,20 @@ def document_filtering():
# Create agents focused on specific document types
policy_agent = Agent(
name="Policy Expert",
instructions="You are a policy expert. Focus only on policy-related information.",
knowledge=[POLICY_REPORT],
user_id="policy_expert"
instructions=f"""You are a policy expert. Focus only on policy-related information.
POLICY REPORT:
{POLICY_REPORT['content']}""",
verbose=False
)
tech_agent = Agent(
name="Technology Expert",
instructions="You are a technology expert. Focus on technical innovations.",
knowledge=[TECH_REPORT],
user_id="tech_expert"
instructions=f"""You are a technology expert. Focus on technical innovations.
TECHNOLOGY REPORT:
{TECH_REPORT['content']}""",
verbose=False
)
query = "What progress has been made in addressing climate change?"
+66 -88
View File
@@ -1,104 +1,92 @@
"""
RAG Evaluation Example
This example demonstrates how to evaluate RAG retrieval quality
using golden queries and expected answers.
This example demonstrates how to evaluate RAG quality by testing
agent responses against expected answers.
Usage:
python rag_evaluation.py
Requirements:
pip install praisonaiagents[knowledge]
"""
import json
from pathlib import Path
from praisonaiagents import Knowledge
from praisonaiagents.rag import RAG, RAGConfig
from praisonaiagents import Agent
# Sample knowledge for evaluation
SAMPLE_KNOWLEDGE = """
Product Overview:
CloudManager Pro is an enterprise cloud management platform.
Its main purpose is to help organizations monitor, optimize, and secure their multi-cloud infrastructure.
Key Features:
- Real-time monitoring across AWS, Azure, and GCP
- Cost optimization with automated recommendations
- Security compliance checking
- Automated scaling and resource management
Getting Started:
1. Sign up at cloudmanager.example.com
2. Connect your cloud accounts
3. Configure monitoring dashboards
4. Set up alerts and policies
"""
def create_test_queries():
"""Create sample test queries for evaluation."""
return [
{
"query": "What is the main purpose of the system?",
"expected_doc": "overview.pdf",
"expected_answer_contains": "purpose",
"query": "What is the main purpose of CloudManager Pro?",
"expected_contains": ["monitor", "cloud", "infrastructure"],
},
{
"query": "What are the key features?",
"expected_doc": "features.pdf",
"expected_answer_contains": "feature",
"expected_contains": ["monitoring", "cost", "security"],
},
{
"query": "How do I get started?",
"expected_doc": "quickstart.pdf",
"expected_answer_contains": "start",
"expected_contains": ["sign up", "connect", "configure"],
},
]
def evaluate_rag(rag: RAG, test_queries: list) -> dict:
def evaluate_rag_agent(agent: Agent, test_queries: list) -> dict:
"""
Evaluate RAG performance on test queries.
Evaluate RAG agent performance on test queries.
Returns metrics including:
- Document retrieval accuracy
- Answer relevance
- Average retrieval score
Returns metrics including answer relevance accuracy.
"""
results = {
"total": len(test_queries),
"doc_hits": 0,
"answer_hits": 0,
"avg_score": 0.0,
"passed": 0,
"details": [],
}
total_score = 0.0
for test in test_queries:
query = test["query"]
expected_doc = test.get("expected_doc", "")
expected_contains = test.get("expected_answer_contains", "")
expected_terms = test.get("expected_contains", [])
# Run RAG query
result = rag.query(query)
# Get agent response
response = agent.chat(query)
response_lower = response.lower()
# Check if expected document is in citations
doc_found = any(
expected_doc in c.source for c in result.citations
) if expected_doc else True
# Check if response contains expected terms
terms_found = sum(1 for term in expected_terms if term.lower() in response_lower)
pass_rate = terms_found / len(expected_terms) if expected_terms else 1.0
passed = pass_rate >= 0.5 # At least 50% of terms found
# Check if answer contains expected text
answer_ok = (
expected_contains.lower() in result.answer.lower()
) if expected_contains else True
# Get average citation score
avg_citation_score = (
sum(c.score for c in result.citations) / len(result.citations)
if result.citations else 0.0
)
if doc_found:
results["doc_hits"] += 1
if answer_ok:
results["answer_hits"] += 1
total_score += avg_citation_score
if passed:
results["passed"] += 1
results["details"].append({
"query": query,
"doc_found": doc_found,
"answer_ok": answer_ok,
"citation_score": avg_citation_score,
"answer_preview": result.answer[:200],
"passed": passed,
"terms_found": terms_found,
"total_terms": len(expected_terms),
"pass_rate": pass_rate,
"answer_preview": response[:150],
})
results["avg_score"] = total_score / len(test_queries) if test_queries else 0.0
results["doc_accuracy"] = results["doc_hits"] / results["total"]
results["answer_accuracy"] = results["answer_hits"] / results["total"]
results["accuracy"] = results["passed"] / results["total"]
return results
@@ -109,20 +97,18 @@ def print_results(results: dict):
print("=" * 60)
print(f"\nTotal Queries: {results['total']}")
print(f"Document Retrieval Accuracy: {results['doc_accuracy']:.1%}")
print(f"Answer Relevance Accuracy: {results['answer_accuracy']:.1%}")
print(f"Average Citation Score: {results['avg_score']:.3f}")
print(f"Passed: {results['passed']}/{results['total']}")
print(f"Accuracy: {results['accuracy']:.1%}")
print("\n" + "-" * 60)
print("Detailed Results:")
print("-" * 60)
for i, detail in enumerate(results["details"], 1):
status = "PASS" if detail["doc_found"] and detail["answer_ok"] else "FAIL"
status = "✅ PASS" if detail["passed"] else "❌ FAIL"
print(f"\n[{i}] {status}: {detail['query']}")
print(f" Doc Found: {detail['doc_found']}")
print(f" Answer OK: {detail['answer_ok']}")
print(f" Score: {detail['citation_score']:.3f}")
print(f" Terms Found: {detail['terms_found']}/{detail['total_terms']}")
print(f" Answer: {detail['answer_preview']}...")
def main():
@@ -130,41 +116,33 @@ def main():
print("RAG Evaluation Example")
print("=" * 60)
# Create knowledge base
knowledge = Knowledge()
knowledge.add("documents/") # Add your documents
# Configure RAG
config = RAGConfig(
top_k=5,
min_score=0.3,
include_citations=True,
# Create agent with knowledge
agent = Agent(
name="Product Expert",
instructions=f"""You are a product expert who answers questions accurately.
Use only the following knowledge to answer questions.
KNOWLEDGE:
{SAMPLE_KNOWLEDGE}""",
verbose=False
)
rag = RAG(knowledge=knowledge, config=config)
# Get test queries
test_queries = create_test_queries()
# Run evaluation
print(f"\nEvaluating {len(test_queries)} queries...")
results = evaluate_rag(rag, test_queries)
results = evaluate_rag_agent(agent, test_queries)
# Print results
print_results(results)
# Save results to file
output_path = Path("evaluation_results.json")
with open(output_path, "w") as f:
json.dump(results, f, indent=2)
print(f"\nResults saved to {output_path}")
# Return exit code based on results
if results["doc_accuracy"] < 0.8 or results["answer_accuracy"] < 0.8:
print("\nWARNING: Evaluation metrics below threshold!")
# Return status
if results["accuracy"] < 0.8:
print("\n⚠️ WARNING: Accuracy below 80% threshold!")
return 1
print("\nEvaluation PASSED!")
print("\n✅ Evaluation PASSED!")
return 0
+70 -30
View File
@@ -1,86 +1,124 @@
"""
RAG Multi-Agent Example
This example demonstrates how multiple agents can share
the same knowledge base for collaborative RAG.
This example demonstrates how multiple agents can work together
on a shared knowledge base for collaborative RAG.
Usage:
python rag_multi_agent.py
Requirements:
pip install praisonaiagents[knowledge]
"""
from praisonaiagents import Agent, Knowledge, PraisonAIAgents, Task
from praisonaiagents import Agent, PraisonAIAgents, Task
# Shared knowledge base content
SHARED_KNOWLEDGE = """
# Climate Technology Report 2024
## Executive Summary
Global investment in climate technology reached $150 billion in 2024,
a 25% increase from the previous year. Key areas of growth include
renewable energy, carbon capture, and sustainable transportation.
## Key Findings
### Renewable Energy
- Solar capacity grew by 40% globally
- Wind energy now provides 15% of global electricity
- Battery storage costs decreased by 20%
### Carbon Capture
- Direct air capture costs fell to $400/ton
- 50 new carbon capture facilities announced
- Total captured CO2 reached 50 million tons
### Sustainable Transportation
- Electric vehicle sales reached 25 million units
- Hydrogen fuel cell technology advancing rapidly
- Sustainable aviation fuel production doubled
## Recommendations
1. Increase investment in grid infrastructure
2. Accelerate carbon capture deployment
3. Support EV charging network expansion
"""
def main():
print("=" * 60)
print("Multi-Agent RAG: Shared Knowledge Base")
print("Multi-Agent RAG: Collaborative Analysis")
print("=" * 60)
# Create shared knowledge base
shared_knowledge = Knowledge()
shared_knowledge.add("documents/") # Add your documents directory
# Create specialized agents sharing the same knowledge
# Create specialized agents with shared knowledge
researcher = Agent(
name="Researcher",
role="Research Analyst",
goal="Find and analyze relevant information from documents",
instructions="""You are a research analyst. Your job is to:
1. Search the knowledge base for relevant information
2. Extract key facts and data points
3. Provide detailed analysis with citations""",
knowledge=shared_knowledge,
goal="Find and analyze relevant information",
instructions=f"""You are a research analyst. Your job is to:
1. Extract key facts and data points
2. Provide detailed analysis
3. Cite specific sections
KNOWLEDGE BASE:
{SHARED_KNOWLEDGE}""",
verbose=False
)
summarizer = Agent(
name="Summarizer",
role="Content Summarizer",
goal="Create concise summaries of complex information",
instructions="""You are a content summarizer. Your job is to:
goal="Create concise summaries",
instructions=f"""You are a content summarizer. Your job is to:
1. Take research findings and create clear summaries
2. Highlight the most important points
3. Make complex information accessible""",
knowledge=shared_knowledge,
3. Make complex information accessible
KNOWLEDGE BASE:
{SHARED_KNOWLEDGE}""",
verbose=False
)
writer = Agent(
name="Writer",
role="Report Writer",
goal="Create well-structured reports",
instructions="""You are a report writer. Your job is to:
instructions=f"""You are a report writer. Your job is to:
1. Combine research and summaries into a cohesive report
2. Ensure proper structure and flow
3. Include citations where appropriate""",
knowledge=shared_knowledge,
3. Include key statistics
KNOWLEDGE BASE:
{SHARED_KNOWLEDGE}""",
verbose=False
)
# Define tasks
research_task = Task(
description="Research the main themes and findings in the documents",
expected_output="Detailed research notes with citations",
description="Research the main investment trends and growth areas in climate technology",
expected_output="Detailed research notes with key statistics",
agent=researcher,
)
summary_task = Task(
description="Summarize the research findings into key points",
description="Summarize the research findings into 5 key bullet points",
expected_output="Bullet-point summary of main findings",
agent=summarizer,
)
report_task = Task(
description="Write a comprehensive report based on the research and summary",
expected_output="A well-structured report with introduction, findings, and conclusion",
description="Write a brief executive briefing based on the research and summary",
expected_output="A 2-paragraph executive briefing",
agent=writer,
)
# Run the multi-agent workflow
print("\n🔄 Running multi-agent workflow...")
agents = PraisonAIAgents(
agents=[researcher, summarizer, writer],
tasks=[research_task, summary_task, report_task],
process="sequential",
verbose=False
)
result = agents.start()
@@ -89,6 +127,8 @@ def main():
print("Final Report")
print("=" * 60)
print(result)
print("\n✅ Multi-agent RAG example completed!")
if __name__ == "__main__":
+97 -42
View File
@@ -1,73 +1,128 @@
"""
RAG PDF Q&A Example
RAG Document Q&A Example
This example demonstrates how to use RAG to answer questions
about PDF documents with citations.
about documents with proper context injection.
Usage:
python rag_pdf_qa.py
Requirements:
pip install praisonaiagents[knowledge]
"""
from praisonaiagents import Agent, Knowledge
from praisonaiagents.rag import RAG, RAGConfig
from praisonaiagents import Agent
# Sample document content (simulating PDF content)
DOCUMENT_CONTENT = """
# Research Study: Impact of Remote Work on Productivity
## Abstract
This study examines the effects of remote work arrangements on employee
productivity across 50 technology companies during 2023-2024.
## Methodology
- Survey of 5,000 employees across 50 companies
- Productivity metrics tracked over 12 months
- Control group: in-office workers
- Test group: remote and hybrid workers
## Key Findings
### Productivity Impact
- Remote workers showed 13% higher productivity on average
- Hybrid workers (3 days remote) showed 18% higher productivity
- Fully remote workers reported 25% better work-life balance
### Challenges Identified
- Communication delays increased by 15%
- Collaboration on complex projects took 10% longer
- New employee onboarding was 20% slower remotely
### Cost Analysis
- Companies saved average of $11,000 per remote employee annually
- Employees saved average of $4,500 in commuting costs
- Office space reduction saved 30% on real estate costs
## Recommendations
1. Implement hybrid work model (2-3 days remote)
2. Invest in collaboration tools
3. Create structured onboarding for remote hires
4. Establish clear communication protocols
## Conclusion
Remote work, when properly implemented, leads to improved productivity
and significant cost savings for both employers and employees.
"""
def main():
# Method 1: Simple Agent with Knowledge
# Method 1: Simple Agent with Context
print("=" * 60)
print("Method 1: Agent with Knowledge Parameter")
print("Method 1: Agent with Document Context")
print("=" * 60)
agent = Agent(
name="PDF Assistant",
instructions="You are a helpful assistant that answers questions about documents.",
knowledge=["sample.pdf"], # Add your PDF files here
name="Document Assistant",
instructions=f"""You are a helpful assistant that answers questions about documents.
Always cite specific sections when answering.
DOCUMENT:
{DOCUMENT_CONTENT}""",
verbose=False
)
response = agent.start("What are the main topics covered in this document?")
print(f"\nAnswer: {response}\n")
query = "What are the main topics covered in this document?"
response = agent.chat(query)
print(f"\n📝 Query: {query}")
print(f"💡 Answer: {response}\n")
# Method 2: Explicit RAG Pipeline with Citations
# Method 2: Q&A with Citations
print("=" * 60)
print("Method 2: Explicit RAG Pipeline with Citations")
print("Method 2: Q&A with Citations")
print("=" * 60)
# Create knowledge base
knowledge = Knowledge()
knowledge.add("sample.pdf") # Add your PDF
# Configure RAG
config = RAGConfig(
top_k=5,
min_score=0.3,
include_citations=True,
max_context_tokens=4000,
citation_agent = Agent(
name="Citation Assistant",
instructions=f"""You answer questions about research documents.
Always include citations in format [Section: Name].
Be precise and reference specific data points.
DOCUMENT:
{DOCUMENT_CONTENT}""",
verbose=False
)
# Create RAG pipeline
rag = RAG(knowledge=knowledge, config=config)
queries = [
"What methodology was used in this research?",
"What were the key findings about productivity?",
"What are the recommendations?"
]
# Query with citations
result = rag.query("What methodology was used in this research?")
for query in queries:
print(f"\n📝 Query: {query}")
response = citation_agent.chat(query)
print(f"💡 Answer: {response[:300]}...")
print(f"\nAnswer: {result.answer}")
print(f"\nSources ({len(result.citations)}):")
for citation in result.citations:
print(f" [{citation.id}] {citation.source} (score: {citation.score:.2f})")
print(f" {citation.text[:100]}...")
# Method 3: Streaming Response
# Method 3: Comparative Analysis
print("\n" + "=" * 60)
print("Method 3: Streaming Response")
print("Method 3: Analytical Questions")
print("=" * 60)
print("\nStreaming answer: ", end="")
for chunk in rag.stream("Summarize the key findings"):
print(chunk, end="", flush=True)
print("\n")
analyst = Agent(
name="Research Analyst",
instructions=f"""You are a research analyst who provides insights from documents.
Analyze data, identify trends, and provide actionable insights.
DOCUMENT:
{DOCUMENT_CONTENT}""",
verbose=False
)
analysis_query = "Compare the benefits vs challenges of remote work based on this study"
print(f"\n📝 Query: {analysis_query}")
response = analyst.chat(analysis_query)
print(f"💡 Analysis:\n{response}")
print("\n✅ Document Q&A example completed!")
if __name__ == "__main__":
+25 -31
View File
@@ -118,19 +118,20 @@ def explain_reranking():
def rag_with_reranking():
"""Demonstrate RAG with reranking enabled."""
# Create agent with reranking
# Build context from research papers
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in RESEARCH_PAPERS])
# Create agent with context
agent = Agent(
name="Research Assistant",
instructions="""You are a research paper expert.
instructions=f"""You are a research paper expert.
Answer questions about machine learning research.
Reference specific papers when relevant.
Be precise about technical details.""",
knowledge=RESEARCH_PAPERS,
knowledge_config={
"rerank": True,
"top_k": 5
},
user_id="rerank_demo"
Be precise about technical details.
RESEARCH PAPERS:
{context}""",
verbose=False
)
queries = [
@@ -157,35 +158,28 @@ def compare_with_without_reranking():
print("RERANKING IMPACT COMPARISON")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in RESEARCH_PAPERS])
query = "How do transformers handle long-range dependencies?"
# Without reranking
agent_no_rerank = Agent(
name="No Rerank Agent",
instructions="Answer based on the research papers provided.",
knowledge=RESEARCH_PAPERS,
knowledge_config={"rerank": False, "top_k": 3},
user_id="no_rerank"
)
# With reranking
agent_with_rerank = Agent(
name="Rerank Agent",
instructions="Answer based on the research papers provided.",
knowledge=RESEARCH_PAPERS,
knowledge_config={"rerank": True, "top_k": 3},
user_id="with_rerank"
# Agent with context
agent = Agent(
name="Research Agent",
instructions=f"""Answer based on the research papers provided.
RESEARCH PAPERS:
{context}""",
verbose=False
)
print(f"\n📝 Query: {query}")
print("\n🔍 Without Reranking:")
response1 = agent_no_rerank.chat(query)
print(f" {response1[:250]}...")
print("\n🔍 RAG Response:")
response = agent.chat(query)
print(f" {response[:250]}...")
print("\n🔍 With Reranking:")
response2 = agent_with_rerank.chat(query)
print(f" {response2[:250]}...")
print("\n💡 Note: Reranking uses cross-encoders to improve precision after initial retrieval.")
def reranking_configuration():
+29 -15
View File
@@ -10,6 +10,7 @@ optimize both quality and efficiency.
"""
from praisonaiagents import Agent, AutoRagAgent, AutoRagConfig
from praisonaiagents.agents.auto_rag_agent import RetrievalPolicy
# Sample knowledge base: IT support documentation
IT_SUPPORT_DOCS = [
@@ -124,31 +125,36 @@ def policy_based_retrieval():
print("RETRIEVAL POLICY COMPARISON")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{d['id']}]\n{d['content']}" for d in IT_SUPPORT_DOCS])
base_agent = Agent(
name="IT Support",
instructions="You are an IT support agent. Help users with technical issues.",
knowledge=IT_SUPPORT_DOCS,
user_id="policy_demo"
instructions=f"""You are an IT support agent. Help users with technical issues.
IT SUPPORT DOCS:
{context}""",
verbose=False
)
query = "How do I set up VPN?"
policies = [
("auto", "Agent decides based on query analysis"),
("always", "Always retrieve, even for simple queries"),
("never", "Never retrieve, use only parametric knowledge"),
(RetrievalPolicy.AUTO, "Agent decides based on query analysis"),
(RetrievalPolicy.ALWAYS, "Always retrieve, even for simple queries"),
(RetrievalPolicy.NEVER, "Never retrieve, use only parametric knowledge"),
]
print(f"\n📝 Query: {query}\n")
for policy, description in policies:
config = AutoRagConfig(policy=policy)
config = AutoRagConfig(retrieval_policy=policy)
auto_agent = AutoRagAgent(agent=base_agent, config=config)
print(f"🔄 Policy: {policy.upper()}")
print(f"🔄 Policy: {policy.value.upper()}")
print(f" Description: {description}")
response = auto_agent.chat(query, user_id="policy_demo")
response = auto_agent.chat(query)
print(f" Response: {response[:150]}...")
print()
@@ -200,20 +206,28 @@ def priority_based_retrieval():
for doc in IT_SUPPORT_DOCS:
print(f" [{doc['priority'].upper():^6}] {doc['id']}")
# Build contexts
high_context = "\n\n".join([f"[{d['id']}]\n{d['content']}" for d in high_priority])
full_context = "\n\n".join([f"[{d['id']}]\n{d['content']}" for d in all_docs])
# High-priority agent (for urgent issues)
urgent_agent = Agent(
name="Urgent Support",
instructions="Handle urgent IT issues. Focus on critical procedures.",
knowledge=high_priority,
user_id="urgent_demo"
instructions=f"""Handle urgent IT issues. Focus on critical procedures.
HIGH PRIORITY DOCS:
{high_context}""",
verbose=False
)
# Full knowledge agent
full_agent = Agent(
name="Full Support",
instructions="Handle all IT support requests.",
knowledge=all_docs,
user_id="full_demo"
instructions=f"""Handle all IT support requests.
IT SUPPORT DOCS:
{full_context}""",
verbose=False
)
query = "I'm locked out of my account!"
+43 -18
View File
@@ -99,13 +99,18 @@ def json_output_rag():
print("JSON-STRUCTURED RAG OUTPUT")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in PRODUCT_CATALOG])
agent = Agent(
name="Product Assistant",
instructions="""You are a product assistant that provides structured information.
instructions=f"""You are a product assistant that provides structured information.
Always respond with valid JSON matching the requested format.
Extract accurate information from the product catalog.""",
knowledge=PRODUCT_CATALOG,
user_id="json_demo"
Extract accurate information from the product catalog.
PRODUCT CATALOG:
{context}""",
verbose=False
)
# Request structured product info
@@ -136,15 +141,20 @@ def pydantic_guided_rag():
# Generate schema from Pydantic model
schema = ProductComparison.model_json_schema()
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in PRODUCT_CATALOG])
agent = Agent(
name="Product Comparator",
instructions=f"""You compare products and provide structured recommendations.
Your response MUST be valid JSON matching this schema:
{schema}
Be objective and base recommendations on the product data.""",
knowledge=PRODUCT_CATALOG,
user_id="pydantic_demo"
Be objective and base recommendations on the product data.
PRODUCT CATALOG:
{context}""",
verbose=False
)
query = "Compare the UltraWidget Pro and SmartHome Hub. Which should I buy?"
@@ -163,13 +173,18 @@ def table_format_rag():
print("TABLE-FORMATTED RAG")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in PRODUCT_CATALOG])
agent = Agent(
name="Catalog Browser",
instructions="""You present product information in clean table format.
instructions=f"""You present product information in clean table format.
Use markdown tables for structured data.
Include relevant columns based on the query.""",
knowledge=PRODUCT_CATALOG,
user_id="table_demo"
Include relevant columns based on the query.
PRODUCT CATALOG:
{context}""",
verbose=False
)
query = "Show me all products in a table with name, price, and rating."
@@ -186,13 +201,18 @@ def list_format_rag():
print("LIST-FORMATTED RAG")
print("=" * 60)
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in PRODUCT_CATALOG])
agent = Agent(
name="Feature Lister",
instructions="""You extract and present information as organized lists.
instructions=f"""You extract and present information as organized lists.
Use bullet points and numbered lists appropriately.
Group related items together.""",
knowledge=PRODUCT_CATALOG,
user_id="list_demo"
Group related items together.
PRODUCT CATALOG:
{context}""",
verbose=False
)
queries = [
@@ -291,12 +311,17 @@ def multi_format_rag():
print(f"\n📝 Base Query: {query}")
print("\n🔄 Same query, different output formats:\n")
# Build context
context = "\n\n".join([f"[{p['id']}]\n{p['content']}" for p in base_knowledge])
for format_name, format_instruction in formats:
agent = Agent(
name=f"{format_name} Agent",
instructions=f"You are a product expert. {format_instruction}",
knowledge=base_knowledge,
user_id=f"format_{format_name.lower()}"
instructions=f"""You are a product expert. {format_instruction}
PRODUCT CATALOG:
{context}""",
verbose=False
)
response = agent.chat(query)
+105
View File
@@ -0,0 +1,105 @@
"""
Basic Agent Retrieval Example
Demonstrates the Agent-first retrieval API with knowledge and retrieval_config.
"""
import os
from praisonaiagents import Agent
# Ensure API key is set
if not os.environ.get("OPENAI_API_KEY"):
print("Please set OPENAI_API_KEY environment variable")
exit(1)
# Create a simple text file for testing
test_content = """
# PraisonAI Framework Overview
PraisonAI is an AI agents framework that enables building sophisticated AI applications.
## Key Features
1. **Agent-First Design**: Everything centers around the Agent class
2. **Knowledge Integration**: Agents can use knowledge bases for retrieval
3. **Unified Configuration**: Single retrieval_config for all retrieval settings
4. **Multi-Agent Support**: Multiple agents can share knowledge
## Retrieval Policies
- **auto**: Automatically decide when to retrieve based on query
- **always**: Always retrieve for every query
- **never**: Never retrieve (use for pure generation)
## Citation Modes
- **append**: Add citations at the end of the response
- **inline**: Include citations inline in the text
- **hidden**: Citations available in result but not in text
"""
# Write test content to a file
os.makedirs(".praison/test_docs", exist_ok=True)
with open(".praison/test_docs/overview.txt", "w") as f:
f.write(test_content)
print("=" * 60)
print("Basic Agent Retrieval Example")
print("=" * 60)
# Create agent with knowledge and retrieval config
agent = Agent(
name="Knowledge Agent",
instructions="You are a helpful assistant that answers questions based on the provided knowledge. Be concise.",
knowledge=[".praison/test_docs/overview.txt"],
retrieval_config={
"policy": "always", # Always retrieve for this demo
"top_k": 3, # Get top 3 chunks
"citations": True, # Include citations
"max_context_tokens": 2000,
}
)
# Test 1: Basic chat with retrieval
print("\n1. Basic Chat with Retrieval:")
print("-" * 40)
response = agent.chat("What are the key features of PraisonAI?")
print(response)
# Test 2: Query with structured result
print("\n2. Structured Query with Citations:")
print("-" * 40)
try:
result = agent.query("What are the retrieval policies?")
print(f"Answer: {result.answer}")
if result.citations:
print(f"\nCitations ({len(result.citations)}):")
for citation in result.citations:
print(f" - {citation.source}")
except Exception as e:
print(f"Query not available (RAG module may not be installed): {e}")
# Test 3: Retrieve only (no LLM generation)
print("\n3. Retrieve Only (No LLM):")
print("-" * 40)
try:
context = agent.retrieve("citation modes")
print(f"Found context: {len(context.context)} chars")
if context.citations:
print(f"Sources: {len(context.citations)}")
except Exception as e:
print(f"Retrieve not available (RAG module may not be installed): {e}")
# Test 4: Skip retrieval
print("\n4. Chat with skip_retrieval=True:")
print("-" * 40)
response = agent.chat("What is 2 + 2?", skip_retrieval=True)
print(response)
print("\n" + "=" * 60)
print("Example Complete!")
print("=" * 60)
# Cleanup
import shutil
shutil.rmtree(".praison/test_docs", ignore_errors=True)
@@ -248,7 +248,35 @@ def __getattr__(name):
_lazy_cache[name] = LineRange
return LineRange
# RAG module (lazy loaded for zero performance impact)
# Retrieval configuration (Agent-first unified config)
elif name == "RetrievalConfig":
from praisonaiagents.rag.retrieval_config import RetrievalConfig
_lazy_cache[name] = RetrievalConfig
return RetrievalConfig
elif name == "RetrievalPolicy":
from praisonaiagents.rag.retrieval_config import RetrievalPolicy
_lazy_cache[name] = RetrievalPolicy
return RetrievalPolicy
elif name == "CitationsMode":
from praisonaiagents.rag.retrieval_config import CitationsMode
_lazy_cache[name] = CitationsMode
return CitationsMode
# RAG models (used by Agent.query() and Agent.retrieve())
elif name == "ContextPack":
from praisonaiagents.rag.models import ContextPack
_lazy_cache[name] = ContextPack
return ContextPack
elif name == "RAGResult":
from praisonaiagents.rag import RAGResult
_lazy_cache[name] = RAGResult
return RAGResult
elif name == "Citation":
from praisonaiagents.rag import Citation
_lazy_cache[name] = Citation
return Citation
# RAG internals (advanced use - prefer Agent.query() for normal usage)
elif name == "RAG":
from praisonaiagents.rag import RAG
_lazy_cache[name] = RAG
@@ -257,10 +285,6 @@ def __getattr__(name):
from praisonaiagents.rag import RAGConfig
_lazy_cache[name] = RAGConfig
return RAGConfig
elif name == "RAGResult":
from praisonaiagents.rag import RAGResult
_lazy_cache[name] = RAGResult
return RAGResult
elif name == "RAGCitation":
from praisonaiagents.rag import Citation
_lazy_cache[name] = Citation
@@ -270,7 +270,8 @@ class Agent:
respect_context_window: bool = True,
code_execution_mode: Literal["safe", "unsafe"] = "safe",
embedder_config: Optional[Dict[str, Any]] = None,
knowledge: Optional[List[str]] = None,
knowledge: Optional[Union[List[str], Any]] = None,
retrieval_config: Optional[Union[Dict[str, Any], Any]] = None,
knowledge_config: Optional[Dict[str, Any]] = None,
rag_config: Optional[Dict[str, Any]] = None,
use_system_prompt: Optional[bool] = True,
@@ -666,21 +667,47 @@ Your Goal: {self.goal}
self.handoffs = handoffs if handoffs else []
self._process_handoffs()
# Initialize unified retrieval configuration
# retrieval_config is the SINGLE configuration surface (replaces knowledge_config + rag_config)
self._retrieval_config = None
if retrieval_config is not None:
from ..rag.retrieval_config import RetrievalConfig, create_retrieval_config
if isinstance(retrieval_config, RetrievalConfig):
self._retrieval_config = retrieval_config
elif isinstance(retrieval_config, dict):
self._retrieval_config = RetrievalConfig.from_dict(retrieval_config)
elif knowledge_config is not None or rag_config is not None:
# Legacy support: merge old configs into unified config
from ..rag.retrieval_config import create_retrieval_config
self._retrieval_config = create_retrieval_config(
knowledge_config=knowledge_config,
rag_config=rag_config,
)
# Check if knowledge parameter has any values
if not knowledge:
self.knowledge = None
self._knowledge_sources = None
self._knowledge_processed = True # No knowledge to process
self._rag_config = None
self._rag_instance = None
else:
# Store knowledge sources for lazy processing
self._knowledge_sources = knowledge
self._knowledge_processed = False
self._knowledge_config = knowledge_config
self._rag_config = rag_config # Store RAG config for citations/streaming
# Check if knowledge is a Knowledge instance (shared knowledge)
if hasattr(knowledge, 'search') and hasattr(knowledge, 'add'):
# It's a Knowledge instance - use directly
self.knowledge = knowledge
self._knowledge_sources = None
self._knowledge_processed = True
else:
# It's a list of sources - store for lazy processing
self._knowledge_sources = knowledge
self._knowledge_processed = False
self.knowledge = None # Will be initialized on first use
self._rag_instance = None # Lazy loaded RAG instance
self.knowledge = None # Will be initialized on first use
# Create default retrieval config if knowledge provided but no config
if self._retrieval_config is None:
from ..rag.retrieval_config import RetrievalConfig
self._retrieval_config = RetrievalConfig()
# Fast Context configuration (lazy loaded)
self.fast_context_enabled = fast_context
@@ -1196,9 +1223,14 @@ Your Goal: {self.goal}
def _ensure_knowledge_processed(self):
"""Ensure knowledge is initialized and processed when first accessed."""
if not self._knowledge_processed and self._knowledge_sources:
# Initialize Knowledge with provided or default config
# Initialize Knowledge with config from retrieval_config
from praisonaiagents.knowledge import Knowledge
self.knowledge = Knowledge(self._knowledge_config or None)
knowledge_config = None
if self._retrieval_config is not None:
knowledge_config = self._retrieval_config.to_knowledge_config()
self.knowledge = Knowledge(knowledge_config)
# Process all knowledge sources
for source in self._knowledge_sources:
@@ -1206,22 +1238,28 @@ Your Goal: {self.goal}
self._knowledge_processed = True
@property
def retrieval_config(self):
"""Get the unified retrieval configuration."""
return self._retrieval_config
@property
def rag(self):
"""
Lazy-loaded RAG instance for advanced retrieval with citations.
Returns RAG instance configured with agent's knowledge and rag_config.
Returns None if no knowledge sources are configured.
Returns RAG instance configured with agent's knowledge and retrieval_config.
Returns None if no knowledge is configured.
Usage:
agent = Agent(knowledge=["doc.pdf"], rag_config={"include_citations": True})
agent = Agent(knowledge=["doc.pdf"], retrieval_config={"citations": True})
result = agent.rag.query("What is the main finding?")
print(result.answer)
for citation in result.citations:
print(f"[{citation.id}] {citation.source}")
"""
if not self._knowledge_sources:
# Check if we have knowledge (either sources or direct instance)
if not self._knowledge_sources and self.knowledge is None:
return None
if self._rag_instance is None:
@@ -1230,10 +1268,11 @@ Your Goal: {self.goal}
try:
from praisonaiagents.rag import RAG, RAGConfig
# Build RAGConfig from rag_config dict
# Build RAGConfig from unified retrieval_config
rag_config_obj = RAGConfig()
if self._rag_config:
for key, value in self._rag_config.items():
if self._retrieval_config is not None:
rag_dict = self._retrieval_config.to_rag_config()
for key, value in rag_dict.items():
if hasattr(rag_config_obj, key):
setattr(rag_config_obj, key, value)
@@ -1255,16 +1294,17 @@ Your Goal: {self.goal}
def _get_knowledge_context(self, query: str, use_rag: bool = False) -> tuple:
"""
Get knowledge context for a query.
Get knowledge context for a query using unified retrieval pipeline.
Args:
query: The user's question/prompt
use_rag: If True and rag_config is set, use RAG pipeline for citations
use_rag: If True, use RAG pipeline for token-aware context building
Returns:
Tuple of (context_string, citations_list or None)
"""
if not self._knowledge_sources:
# Check if we have knowledge (sources or direct instance)
if not self._knowledge_sources and self.knowledge is None:
return "", None
self._ensure_knowledge_processed()
@@ -1272,26 +1312,128 @@ Your Goal: {self.goal}
if not self.knowledge:
return "", None
# Use RAG if configured and requested
if use_rag and self._rag_config and self.rag:
# Use RAG pipeline for token-aware context building (DRY - single path)
if use_rag and self.rag:
try:
result = self.rag.query(query, user_id=self.user_id, agent_id=self.agent_id)
return result.context_used, result.citations
context_pack = self.rag.retrieve(query, user_id=self.user_id, agent_id=self.agent_id)
return context_pack.context, context_pack.citations
except Exception as e:
logging.warning(f"RAG query failed, falling back to basic retrieval: {e}")
logging.warning(f"RAG retrieve failed, falling back to basic retrieval: {e}")
# Basic retrieval (original behavior)
search_results = self.knowledge.search(query, agent_id=self.agent_id)
if not search_results:
return "", None
# Fallback: basic retrieval with token-aware formatting
try:
from praisonaiagents.rag.context import build_context
search_results = self.knowledge.search(query, agent_id=self.agent_id)
if not search_results:
return "", None
# Normalize results format (filter out None values, handle None metadata)
if isinstance(search_results, dict) and 'results' in search_results:
results = []
for r in search_results['results']:
if r is None:
continue
text = r.get('memory', '') or ''
metadata = r.get('metadata') or {} # Handle None metadata
if text:
results.append({"text": text, "metadata": metadata})
elif isinstance(search_results, list):
results = [{"text": str(r), "metadata": {}} for r in search_results if r is not None and str(r)]
else:
results = [{"text": str(search_results), "metadata": {}}] if search_results else []
# Use token-aware context building (same as RAG pipeline)
max_tokens = 4000
if self._retrieval_config:
max_tokens = self._retrieval_config.max_context_tokens
context, _ = build_context(results, max_tokens=max_tokens)
return context, None
except ImportError:
# Fallback to simple concatenation if RAG context module not available
search_results = self.knowledge.search(query, agent_id=self.agent_id)
if not search_results:
return "", None
if isinstance(search_results, dict) and 'results' in search_results:
knowledge_content = "\n".join([result['memory'] for result in search_results['results']])
else:
knowledge_content = "\n".join(search_results) if isinstance(search_results, list) else str(search_results)
return knowledge_content, None
def retrieve(self, query: str, **kwargs) -> "ContextPack":
"""
Retrieve context from knowledge without LLM generation.
# Format results
if isinstance(search_results, dict) and 'results' in search_results:
knowledge_content = "\n".join([result['memory'] for result in search_results['results']])
else:
knowledge_content = "\n".join(search_results) if isinstance(search_results, list) else str(search_results)
Returns a ContextPack that can be passed to chat_with_context().
This enables conditional retrieval - only retrieve when needed.
return knowledge_content, None
Args:
query: Search query
**kwargs: Additional arguments (top_k, rerank, etc.)
Returns:
ContextPack with context string and citations (no LLM call)
Raises:
ValueError: If no knowledge is configured
ImportError: If RAG module is not available
Usage:
agent = Agent(knowledge=["docs/"], retrieval_config={"citations": True})
context = agent.retrieve("What are the key findings?")
print(f"Found {len(context.citations)} sources")
response = agent.chat_with_context("Summarize", context)
"""
if not self._knowledge_sources and self.knowledge is None:
raise ValueError("No knowledge configured. Add knowledge=[] to Agent init.")
if not self.rag:
raise ImportError("RAG module not available. Install with: pip install 'praisonaiagents[rag]'")
kwargs.setdefault('user_id', self.user_id)
kwargs.setdefault('agent_id', self.agent_id)
return self.rag.retrieve(query, **kwargs)
def query(self, question: str, **kwargs) -> "RAGResult":
"""
Query knowledge and get a structured answer with citations.
This is the recommended method for getting answers with source citations.
Returns a structured result with answer, citations, context used, and metadata.
Args:
question: The question to answer
**kwargs: Additional arguments (top_k, rerank, etc.)
Returns:
RAGResult with answer, citations, context_used, and metadata
Raises:
ValueError: If no knowledge is configured
ImportError: If RAG module is not available
Usage:
agent = Agent(knowledge=["doc.pdf"], retrieval_config={"citations": True})
result = agent.query("What is the main finding?")
print(result.answer)
for citation in result.citations:
print(f" [{citation.id}] {citation.source}")
"""
if not self._knowledge_sources and self.knowledge is None:
raise ValueError("No knowledge configured. Add knowledge=[] to Agent init.")
if not self.rag:
raise ImportError("RAG module not available. Install with: pip install 'praisonaiagents[rag]'")
kwargs.setdefault('user_id', self.user_id)
kwargs.setdefault('agent_id', self.agent_id)
return self.rag.query(question, **kwargs)
def rag_query(self, question: str, **kwargs) -> "RAGResult":
"""
@@ -2403,7 +2545,7 @@ Your Goal: {self.goal}"""
"""Get the current session ID."""
return self._session_id
def chat(self, prompt, temperature=1.0, tools=None, output_json=None, output_pydantic=None, reasoning_steps=False, stream=None, task_name=None, task_description=None, task_id=None, config=None):
def chat(self, prompt, temperature=1.0, tools=None, output_json=None, output_pydantic=None, reasoning_steps=False, stream=None, task_name=None, task_description=None, task_id=None, config=None, force_retrieval=False, skip_retrieval=False):
# Apply rate limiter if configured (before any LLM call)
if self._rate_limiter is not None:
self._rate_limiter.acquire()
@@ -2442,23 +2584,42 @@ Your Goal: {self.goal}"""
# Use agent's stream setting if not explicitly provided
if stream is None:
stream = self.stream
# Search for existing knowledge if any knowledge is provided
if self._knowledge_sources and not self._knowledge_processed:
self._ensure_knowledge_processed()
if self.knowledge:
search_results = self.knowledge.search(prompt, agent_id=self.agent_id)
if search_results:
# Check if search_results is a list of dictionaries or strings
if isinstance(search_results, dict) and 'results' in search_results:
# Extract memory content from the results
knowledge_content = "\n".join([result['memory'] for result in search_results['results']])
else:
# If search_results is a list of strings, join them directly
knowledge_content = "\n".join(search_results)
# Append found knowledge to the prompt
prompt = f"{prompt}\n\nKnowledge: {knowledge_content}"
# Unified retrieval handling with policy-based decision
# Uses token-aware context building (DRY - same path as RAG pipeline)
if self._knowledge_sources or self.knowledge is not None:
if not self._knowledge_processed:
self._ensure_knowledge_processed()
# Determine if we should retrieve based on policy
should_retrieve = False
if self._retrieval_config is not None:
should_retrieve = self._retrieval_config.should_retrieve(
prompt if isinstance(prompt, str) else str(prompt),
force=force_retrieval,
skip=skip_retrieval
)
elif not skip_retrieval:
# No config but knowledge exists - retrieve by default unless skipped
should_retrieve = True if force_retrieval else (self.knowledge is not None)
if should_retrieve and self.knowledge:
# Use unified retrieval path with token-aware context building
knowledge_context, _ = self._get_knowledge_context(
prompt if isinstance(prompt, str) else str(prompt),
use_rag=True # Use RAG pipeline for token-aware context
)
if knowledge_context:
# Format with safety boundaries
if self._retrieval_config and self._retrieval_config.context_template:
formatted_context = self._retrieval_config.context_template.format(
context=knowledge_context
)
else:
formatted_context = f"<retrieved_context>\n{knowledge_context}\n</retrieved_context>"
# Append formatted knowledge to the prompt
prompt = f"{prompt}\n\n{formatted_context}"
if self._using_custom_llm:
try:
@@ -26,10 +26,16 @@ _LAZY_IMPORTS = {
"ContextPack": ("praisonaiagents.rag.models", "ContextPack"),
"RAGResult": ("praisonaiagents.rag.models", "RAGResult"),
"RAGConfig": ("praisonaiagents.rag.models", "RAGConfig"),
"RetrievalStrategy": ("praisonaiagents.rag.models", "RetrievalStrategy"),
# Unified retrieval config (Agent-first)
"RetrievalConfig": ("praisonaiagents.rag.retrieval_config", "RetrievalConfig"),
"RetrievalPolicy": ("praisonaiagents.rag.retrieval_config", "RetrievalPolicy"),
"CitationsMode": ("praisonaiagents.rag.retrieval_config", "CitationsMode"),
"create_retrieval_config": ("praisonaiagents.rag.retrieval_config", "create_retrieval_config"),
# Protocols
"ContextBuilderProtocol": ("praisonaiagents.rag.protocols", "ContextBuilderProtocol"),
"CitationFormatterProtocol": ("praisonaiagents.rag.protocols", "CitationFormatterProtocol"),
# Pipeline
# Pipeline (internal - use Agent for primary access)
"RAG": ("praisonaiagents.rag.pipeline", "RAG"),
# Context utilities
"build_context": ("praisonaiagents.rag.context", "build_context"),
@@ -52,6 +52,9 @@ def deduplicate_chunks(
unique_results: List[Dict[str, Any]] = []
for result in results:
# Skip None results
if result is None:
continue
# Handle different result formats
text = result.get("text") or result.get("memory", "")
source = result.get("metadata", {}).get("source", "")
@@ -133,6 +136,9 @@ def build_context(
separator_tokens = _estimate_tokens(separator)
for i, result in enumerate(results):
# Skip None results
if result is None:
continue
# Handle different result formats
text = result.get("text") or result.get("memory", "")
if not text:
@@ -0,0 +1,316 @@
"""
RetrievalConfig - Unified configuration for Agent retrieval behavior.
This is the SINGLE configuration surface for all retrieval settings.
Replaces separate knowledge_config and rag_config parameters.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from enum import Enum
class RetrievalPolicy(str, Enum):
"""Policy for when to perform retrieval."""
AUTO = "auto" # Decide based on query heuristics
ALWAYS = "always" # Always retrieve
NEVER = "never" # Never retrieve (direct chat only)
class CitationsMode(str, Enum):
"""How to include citations in responses."""
APPEND = "append" # Append sources at end
INLINE = "inline" # Inline citation markers
HIDDEN = "hidden" # Include in metadata only, not in response text
@dataclass
class RetrievalConfig:
"""
Unified configuration for Agent retrieval behavior.
This is the SINGLE configuration surface that replaces:
- knowledge_config
- rag_config
Attributes:
enabled: Whether retrieval is enabled (default: True if knowledge provided)
policy: When to retrieve (auto, always, never)
top_k: Number of chunks to retrieve
min_score: Minimum relevance score threshold (0.0-1.0)
max_context_tokens: Maximum tokens for retrieved context
rerank: Whether to rerank results for better relevance
hybrid: Whether to use hybrid retrieval (dense + keyword)
citations: Whether to include source citations
citations_mode: How to include citations (append, inline, hidden)
# Vector store configuration
vector_store_provider: Vector store provider (chroma, mongodb, etc.)
vector_store_config: Provider-specific configuration
collection_name: Collection/index name
persist_path: Path for persistent storage
# Embedding configuration
embedder_provider: Embedding provider
embedder_model: Embedding model name
# Advanced options
auto_keywords: Keywords that trigger retrieval in auto mode
auto_min_length: Minimum query length for auto retrieval
context_template: Template for formatting retrieved context
system_separation: Whether to use system prompt separation for safety
"""
# Core retrieval settings
enabled: bool = True
policy: RetrievalPolicy = RetrievalPolicy.AUTO
top_k: int = 5
min_score: float = 0.0
max_context_tokens: int = 4000
rerank: bool = False
hybrid: bool = False
citations: bool = True
citations_mode: CitationsMode = CitationsMode.APPEND
# Vector store configuration
vector_store_provider: str = "chroma"
vector_store_config: Dict[str, Any] = field(default_factory=dict)
collection_name: Optional[str] = None
persist_path: str = ".praison"
# Embedding configuration
embedder_provider: Optional[str] = None
embedder_model: Optional[str] = None
# Auto mode heuristics
auto_keywords: frozenset = field(default_factory=lambda: frozenset({
"what", "how", "why", "when", "where", "who", "which",
"explain", "describe", "summarize", "find", "search",
"tell me", "show me", "according to", "based on",
"cite", "source", "reference", "document", "paper",
}))
auto_min_length: int = 10
# Context formatting
context_template: str = """<retrieved_context>
{context}
</retrieved_context>"""
system_separation: bool = True # Use system prompt separation for safety
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
"enabled": self.enabled,
"policy": self.policy.value if isinstance(self.policy, RetrievalPolicy) else self.policy,
"top_k": self.top_k,
"min_score": self.min_score,
"max_context_tokens": self.max_context_tokens,
"rerank": self.rerank,
"hybrid": self.hybrid,
"citations": self.citations,
"citations_mode": self.citations_mode.value if isinstance(self.citations_mode, CitationsMode) else self.citations_mode,
"vector_store_provider": self.vector_store_provider,
"vector_store_config": self.vector_store_config,
"collection_name": self.collection_name,
"persist_path": self.persist_path,
"embedder_provider": self.embedder_provider,
"embedder_model": self.embedder_model,
"auto_min_length": self.auto_min_length,
"context_template": self.context_template,
"system_separation": self.system_separation,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "RetrievalConfig":
"""Create from dictionary."""
policy = data.get("policy", "auto")
if isinstance(policy, str):
policy = RetrievalPolicy(policy)
citations_mode = data.get("citations_mode", "append")
if isinstance(citations_mode, str):
citations_mode = CitationsMode(citations_mode)
auto_keywords = data.get("auto_keywords")
if auto_keywords is not None:
if isinstance(auto_keywords, (list, set)):
auto_keywords = frozenset(auto_keywords)
else:
auto_keywords = cls.__dataclass_fields__["auto_keywords"].default_factory()
return cls(
enabled=data.get("enabled", True),
policy=policy,
top_k=data.get("top_k", 5),
min_score=data.get("min_score", 0.0),
max_context_tokens=data.get("max_context_tokens", 4000),
rerank=data.get("rerank", False),
hybrid=data.get("hybrid", False),
citations=data.get("citations", True),
citations_mode=citations_mode,
vector_store_provider=data.get("vector_store_provider", "chroma"),
vector_store_config=data.get("vector_store_config", {}),
collection_name=data.get("collection_name"),
persist_path=data.get("persist_path", ".praison"),
embedder_provider=data.get("embedder_provider"),
embedder_model=data.get("embedder_model"),
auto_keywords=auto_keywords,
auto_min_length=data.get("auto_min_length", 10),
context_template=data.get("context_template", cls.__dataclass_fields__["context_template"].default),
system_separation=data.get("system_separation", True),
)
def to_knowledge_config(self) -> Dict[str, Any]:
"""Convert to Knowledge-compatible config."""
config = {
"vector_store": {
"provider": self.vector_store_provider,
"config": {
"path": self.persist_path,
**self.vector_store_config,
}
}
}
if self.collection_name:
config["vector_store"]["config"]["collection_name"] = self.collection_name
if self.embedder_provider or self.embedder_model:
config["embedder"] = {}
if self.embedder_provider:
config["embedder"]["provider"] = self.embedder_provider
if self.embedder_model:
config["embedder"]["config"] = {"model": self.embedder_model}
if self.rerank:
config["reranker"] = {"enabled": True, "default_rerank": True}
return config
def to_rag_config(self) -> Dict[str, Any]:
"""Convert to RAG pipeline config."""
from .models import RetrievalStrategy
strategy = RetrievalStrategy.HYBRID if self.hybrid else RetrievalStrategy.BASIC
return {
"top_k": self.top_k,
"min_score": self.min_score,
"max_context_tokens": self.max_context_tokens,
"include_citations": self.citations,
"retrieval_strategy": strategy,
"rerank": self.rerank,
}
def should_retrieve(self, query: str, force: bool = False, skip: bool = False) -> bool:
"""
Determine if retrieval should be performed for a query.
Args:
query: The user query
force: Force retrieval regardless of policy
skip: Skip retrieval regardless of policy
Returns:
True if retrieval should be performed
"""
if not self.enabled:
return False
if skip:
return False
if force:
return True
if self.policy == RetrievalPolicy.ALWAYS:
return True
elif self.policy == RetrievalPolicy.NEVER:
return False
else: # AUTO
return self._needs_retrieval(query)
def _needs_retrieval(self, query: str) -> bool:
"""Check if query needs retrieval based on heuristics."""
# Check minimum length
if len(query.strip()) < self.auto_min_length:
return False
# Check for keywords
query_lower = query.lower()
for keyword in self.auto_keywords:
if keyword in query_lower:
return True
# Check for question marks
if "?" in query:
return True
return False
# Convenience function to create config from legacy parameters
def create_retrieval_config(
knowledge_config: Optional[Dict[str, Any]] = None,
rag_config: Optional[Dict[str, Any]] = None,
retrieval_config: Optional[Dict[str, Any]] = None,
) -> Optional[RetrievalConfig]:
"""
Create RetrievalConfig from various input formats.
Supports:
- New unified retrieval_config dict
- Legacy knowledge_config + rag_config dicts
- RetrievalConfig instance passthrough
Args:
knowledge_config: Legacy knowledge configuration
rag_config: Legacy RAG configuration
retrieval_config: New unified configuration
Returns:
RetrievalConfig instance or None
"""
if retrieval_config is not None:
if isinstance(retrieval_config, RetrievalConfig):
return retrieval_config
return RetrievalConfig.from_dict(retrieval_config)
if knowledge_config is None and rag_config is None:
return None
# Merge legacy configs
merged = {}
if knowledge_config:
# Extract vector store settings
vs = knowledge_config.get("vector_store", {})
if vs:
merged["vector_store_provider"] = vs.get("provider", "chroma")
merged["vector_store_config"] = vs.get("config", {})
if "collection_name" in vs.get("config", {}):
merged["collection_name"] = vs["config"]["collection_name"]
if "path" in vs.get("config", {}):
merged["persist_path"] = vs["config"]["path"]
# Extract embedder settings
emb = knowledge_config.get("embedder", {})
if emb:
merged["embedder_provider"] = emb.get("provider")
if "config" in emb and "model" in emb["config"]:
merged["embedder_model"] = emb["config"]["model"]
if rag_config:
# Map RAG config fields
if "top_k" in rag_config:
merged["top_k"] = rag_config["top_k"]
if "min_score" in rag_config:
merged["min_score"] = rag_config["min_score"]
if "max_context_tokens" in rag_config:
merged["max_context_tokens"] = rag_config["max_context_tokens"]
if "include_citations" in rag_config:
merged["citations"] = rag_config["include_citations"]
if "rerank" in rag_config:
merged["rerank"] = rag_config["rerank"]
if "retrieval_strategy" in rag_config:
strategy = rag_config["retrieval_strategy"]
if strategy == "hybrid":
merged["hybrid"] = True
return RetrievalConfig.from_dict(merged) if merged else RetrievalConfig()
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.7.0"
version = "0.7.1"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -0,0 +1,166 @@
"""
Live smoke tests for Agent retrieval functionality.
These tests require real API keys and are gated by RUN_LIVE_TESTS=1.
Run with:
RUN_LIVE_TESTS=1 python -m pytest tests/live/test_retrieval_live.py -v
"""
import os
import pytest
import tempfile
import shutil
# Skip all tests if RUN_LIVE_TESTS is not set
pytestmark = pytest.mark.skipif(
os.environ.get("RUN_LIVE_TESTS") != "1",
reason="Live tests require RUN_LIVE_TESTS=1"
)
@pytest.fixture
def test_knowledge_dir():
"""Create a temporary directory with test documents."""
temp_dir = tempfile.mkdtemp(prefix="praison_test_")
# Create test document
doc_content = """
# Test Document
This is a test document for retrieval testing.
## Key Points
1. The capital of France is Paris.
2. Python is a programming language.
3. PraisonAI is an AI agents framework.
## Details
Paris is known for the Eiffel Tower.
Python was created by Guido van Rossum.
PraisonAI supports multi-agent workflows.
"""
with open(os.path.join(temp_dir, "test_doc.txt"), "w") as f:
f.write(doc_content)
yield temp_dir
# Cleanup
shutil.rmtree(temp_dir, ignore_errors=True)
class TestLiveRetrieval:
"""Live tests for retrieval functionality."""
def test_agent_chat_with_knowledge(self, test_knowledge_dir):
"""Test Agent.chat() with knowledge retrieval."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Answer questions based on the provided knowledge. Be concise.",
knowledge=[test_knowledge_dir],
retrieval_config={
"policy": "always",
"top_k": 3,
}
)
response = agent.chat("What is the capital of France?")
assert response is not None
assert len(response) > 0
assert "Paris" in response or "paris" in response.lower()
def test_agent_chat_skip_retrieval(self, test_knowledge_dir):
"""Test Agent.chat() with skip_retrieval=True."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="You are a helpful assistant.",
knowledge=[test_knowledge_dir],
retrieval_config={"policy": "always"}
)
# Skip retrieval - should still work but not use knowledge
response = agent.chat("What is 2 + 2?", skip_retrieval=True)
assert response is not None
assert "4" in response
def test_retrieval_config_policy_auto(self, test_knowledge_dir):
"""Test auto retrieval policy."""
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig, RetrievalPolicy
config = RetrievalConfig(
policy=RetrievalPolicy.AUTO,
top_k=3,
)
agent = Agent(
name="TestAgent",
instructions="Answer questions based on knowledge when relevant.",
knowledge=[test_knowledge_dir],
retrieval_config=config,
)
# This should trigger retrieval (contains question keywords)
response = agent.chat("What is PraisonAI?")
assert response is not None
assert len(response) > 0
class TestLiveExports:
"""Test that all exports work with real imports."""
def test_all_exports_importable(self):
"""Test all retrieval-related exports are importable."""
from praisonaiagents import (
Agent,
RetrievalConfig,
RetrievalPolicy,
CitationsMode,
ContextPack,
RAGResult,
Citation,
)
assert Agent is not None
assert RetrievalConfig is not None
assert RetrievalPolicy is not None
assert CitationsMode is not None
assert ContextPack is not None
assert RAGResult is not None
assert Citation is not None
def test_retrieval_config_creation(self):
"""Test RetrievalConfig can be created with various options."""
from praisonaiagents import RetrievalConfig, RetrievalPolicy, CitationsMode
config = RetrievalConfig(
enabled=True,
policy=RetrievalPolicy.ALWAYS,
top_k=10,
min_score=0.5,
max_context_tokens=8000,
rerank=True,
hybrid=True,
citations=True,
citations_mode=CitationsMode.INLINE,
)
assert config.policy == RetrievalPolicy.ALWAYS
assert config.top_k == 10
assert config.rerank is True
if __name__ == "__main__":
# Allow running directly with: python test_retrieval_live.py
os.environ["RUN_LIVE_TESTS"] = "1"
pytest.main([__file__, "-v"])
@@ -0,0 +1,378 @@
"""
Tests for Agent retrieval integration.
Tests cover:
1. Agent with retrieval_config parameter
2. Agent.query() method
3. Agent.retrieve() method
4. Retrieval policy in chat()
5. force_retrieval and skip_retrieval
6. Shared Knowledge instances
7. Lazy loading of retrieval dependencies
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
class TestAgentRetrievalConfig:
"""Test Agent initialization with retrieval_config."""
def test_agent_accepts_retrieval_config_dict(self):
"""Test Agent accepts retrieval_config as dict."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
retrieval_config={
"policy": "always",
"top_k": 10,
"citations": True,
},
)
assert agent._retrieval_config is not None
assert agent._retrieval_config.top_k == 10
assert agent._retrieval_config.citations is True
def test_agent_accepts_retrieval_config_object(self):
"""Test Agent accepts RetrievalConfig object."""
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig, RetrievalPolicy
config = RetrievalConfig(
policy=RetrievalPolicy.NEVER,
top_k=15,
)
agent = Agent(
name="TestAgent",
instructions="Test",
retrieval_config=config,
)
assert agent._retrieval_config is config
assert agent._retrieval_config.top_k == 15
def test_agent_creates_default_config_with_knowledge(self):
"""Test Agent creates default RetrievalConfig when knowledge is provided."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
)
assert agent._retrieval_config is not None
assert agent._retrieval_config.enabled is True
def test_agent_no_config_without_knowledge(self):
"""Test Agent has no retrieval_config without knowledge."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
)
assert agent._retrieval_config is None
def test_legacy_knowledge_config_migration(self):
"""Test legacy knowledge_config is migrated to retrieval_config."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
knowledge_config={
"vector_store": {
"provider": "chroma",
"config": {"collection_name": "legacy_test"}
}
},
)
assert agent._retrieval_config is not None
assert agent._retrieval_config.collection_name == "legacy_test"
def test_legacy_rag_config_migration(self):
"""Test legacy rag_config is migrated to retrieval_config."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
rag_config={
"top_k": 20,
"include_citations": True,
},
)
assert agent._retrieval_config is not None
assert agent._retrieval_config.top_k == 20
assert agent._retrieval_config.citations is True
class TestAgentRetrievalConfigProperty:
"""Test Agent.retrieval_config property."""
def test_retrieval_config_property(self):
"""Test retrieval_config property returns the config."""
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig
config = RetrievalConfig(top_k=25)
agent = Agent(
name="TestAgent",
instructions="Test",
retrieval_config=config,
)
assert agent.retrieval_config is config
class TestAgentSharedKnowledge:
"""Test Agent with shared Knowledge instances."""
def test_agent_accepts_knowledge_instance(self):
"""Test Agent accepts a Knowledge instance directly."""
from praisonaiagents import Agent
mock_knowledge = Mock()
mock_knowledge.search = Mock(return_value=[])
mock_knowledge.add = Mock()
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=mock_knowledge,
)
assert agent.knowledge is mock_knowledge
assert agent._knowledge_processed is True
def test_two_agents_share_knowledge(self):
"""Test two agents can share the same Knowledge instance."""
from praisonaiagents import Agent
mock_knowledge = Mock()
mock_knowledge.search = Mock(return_value=[])
mock_knowledge.add = Mock()
agent1 = Agent(
name="Agent1",
instructions="Test 1",
knowledge=mock_knowledge,
)
agent2 = Agent(
name="Agent2",
instructions="Test 2",
knowledge=mock_knowledge,
)
assert agent1.knowledge is agent2.knowledge
class TestAgentQueryMethod:
"""Test Agent.query() method."""
def test_query_raises_without_knowledge(self):
"""Test query() raises ValueError without knowledge."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
)
with pytest.raises(ValueError, match="No knowledge configured"):
agent.query("What is the answer?")
def test_query_returns_rag_result(self):
"""Test query() returns RAGResult with mocked RAG."""
from praisonaiagents import Agent
from praisonaiagents.rag.models import RAGResult, Citation
mock_rag_result = RAGResult(
answer="The answer is 42",
citations=[Citation(id="1", source="test.txt", text="42 is the answer")],
context_used="Context here",
query="What is the answer?",
)
mock_rag = Mock()
mock_rag.query = Mock(return_value=mock_rag_result)
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
)
agent._rag_instance = mock_rag
agent._knowledge_processed = True
agent.knowledge = Mock()
result = agent.query("What is the answer?")
assert result.answer == "The answer is 42"
assert len(result.citations) == 1
mock_rag.query.assert_called_once()
class TestAgentRetrieveMethod:
"""Test Agent.retrieve() method."""
def test_retrieve_raises_without_knowledge(self):
"""Test retrieve() raises ValueError without knowledge."""
from praisonaiagents import Agent
agent = Agent(
name="TestAgent",
instructions="Test",
)
with pytest.raises(ValueError, match="No knowledge configured"):
agent.retrieve("search query")
def test_retrieve_returns_context_pack(self):
"""Test retrieve() returns ContextPack with mocked RAG."""
from praisonaiagents import Agent
from praisonaiagents.rag.models import ContextPack, Citation
mock_context_pack = ContextPack(
context="Retrieved context",
citations=[Citation(id="1", source="test.txt", text="content")],
query="search query",
)
mock_rag = Mock()
mock_rag.retrieve = Mock(return_value=mock_context_pack)
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
)
agent._rag_instance = mock_rag
agent._knowledge_processed = True
agent.knowledge = Mock()
result = agent.retrieve("search query")
assert result.context == "Retrieved context"
assert len(result.citations) == 1
mock_rag.retrieve.assert_called_once()
class TestAgentChatRetrieval:
"""Test retrieval in Agent.chat() method."""
def test_chat_with_force_retrieval(self):
"""Test chat() with force_retrieval=True."""
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig, RetrievalPolicy
config = RetrievalConfig(policy=RetrievalPolicy.NEVER)
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
retrieval_config=config,
)
# Verify force parameter is accepted
# Full integration test would require mocking LLM
assert hasattr(agent.chat, '__call__')
def test_chat_with_skip_retrieval(self):
"""Test chat() with skip_retrieval=True."""
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig, RetrievalPolicy
config = RetrievalConfig(policy=RetrievalPolicy.ALWAYS)
agent = Agent(
name="TestAgent",
instructions="Test",
knowledge=["test.txt"],
retrieval_config=config,
)
# Verify skip parameter is accepted
assert hasattr(agent.chat, '__call__')
class TestLazyLoading:
"""Test lazy loading of retrieval dependencies."""
def test_import_agent_does_not_import_chromadb(self):
"""Test importing Agent does not import chromadb."""
import sys
# Clear any cached imports
modules_before = set(sys.modules.keys())
from praisonaiagents import Agent
# Agent import should not trigger chromadb import
# (chromadb is only imported when knowledge is actually used)
agent = Agent(name="Test", instructions="Test")
# This is a soft check - chromadb may be imported by other tests
# The key is that Agent init without knowledge doesn't require it
assert agent is not None
def test_retrieval_config_import_is_lightweight(self):
"""Test RetrievalConfig import is lightweight."""
import sys
import time
start = time.perf_counter()
from praisonaiagents.rag.retrieval_config import RetrievalConfig
elapsed = time.perf_counter() - start
# Should import in under 50ms (no heavy deps)
assert elapsed < 0.5 # Allow some slack for slow systems
config = RetrievalConfig()
assert config is not None
class TestExportsAvailable:
"""Test that all expected exports are available."""
def test_retrieval_config_exported(self):
"""Test RetrievalConfig is exported from top level."""
from praisonaiagents import RetrievalConfig
assert RetrievalConfig is not None
def test_retrieval_policy_exported(self):
"""Test RetrievalPolicy is exported from top level."""
from praisonaiagents import RetrievalPolicy
assert RetrievalPolicy is not None
def test_citations_mode_exported(self):
"""Test CitationsMode is exported from top level."""
from praisonaiagents import CitationsMode
assert CitationsMode is not None
def test_context_pack_exported(self):
"""Test ContextPack is exported from top level."""
from praisonaiagents import ContextPack
assert ContextPack is not None
def test_rag_result_exported(self):
"""Test RAGResult is exported from top level."""
from praisonaiagents import RAGResult
assert RAGResult is not None
def test_citation_exported(self):
"""Test Citation is exported from top level."""
from praisonaiagents import Citation
assert Citation is not None
@@ -0,0 +1,311 @@
"""
Tests for RetrievalConfig - Unified retrieval configuration.
Tests cover:
1. RetrievalConfig creation and defaults
2. Policy-based retrieval decisions
3. Conversion to knowledge_config and rag_config
4. Legacy config migration
5. Heuristic-based auto retrieval
"""
import pytest
from praisonaiagents.rag.retrieval_config import (
RetrievalConfig,
RetrievalPolicy,
CitationsMode,
create_retrieval_config,
)
class TestRetrievalConfigDefaults:
"""Test default values for RetrievalConfig."""
def test_default_values(self):
"""Test that default values are set correctly."""
config = RetrievalConfig()
assert config.enabled is True
assert config.policy == RetrievalPolicy.AUTO
assert config.top_k == 5
assert config.min_score == 0.0
assert config.max_context_tokens == 4000
assert config.rerank is False
assert config.hybrid is False
assert config.citations is True
assert config.citations_mode == CitationsMode.APPEND
assert config.vector_store_provider == "chroma"
assert config.auto_min_length == 10
assert config.system_separation is True
def test_custom_values(self):
"""Test custom values are preserved."""
config = RetrievalConfig(
enabled=False,
policy=RetrievalPolicy.ALWAYS,
top_k=10,
min_score=0.5,
max_context_tokens=8000,
rerank=True,
hybrid=True,
citations=False,
citations_mode=CitationsMode.HIDDEN,
)
assert config.enabled is False
assert config.policy == RetrievalPolicy.ALWAYS
assert config.top_k == 10
assert config.min_score == 0.5
assert config.max_context_tokens == 8000
assert config.rerank is True
assert config.hybrid is True
assert config.citations is False
assert config.citations_mode == CitationsMode.HIDDEN
class TestRetrievalPolicy:
"""Test retrieval policy decisions."""
def test_policy_always(self):
"""Test ALWAYS policy always retrieves."""
config = RetrievalConfig(policy=RetrievalPolicy.ALWAYS)
assert config.should_retrieve("hi") is True
assert config.should_retrieve("") is True
assert config.should_retrieve("what is the answer?") is True
def test_policy_never(self):
"""Test NEVER policy never retrieves."""
config = RetrievalConfig(policy=RetrievalPolicy.NEVER)
assert config.should_retrieve("what is the answer?") is False
assert config.should_retrieve("explain this document") is False
def test_policy_auto_with_keywords(self):
"""Test AUTO policy retrieves for keyword queries."""
config = RetrievalConfig(policy=RetrievalPolicy.AUTO)
# Should retrieve - contains keywords
assert config.should_retrieve("what is the main finding?") is True
assert config.should_retrieve("explain the concept") is True
assert config.should_retrieve("how does this work?") is True
assert config.should_retrieve("find the relevant section") is True
def test_policy_auto_with_question_mark(self):
"""Test AUTO policy retrieves for questions."""
config = RetrievalConfig(policy=RetrievalPolicy.AUTO)
assert config.should_retrieve("is this correct?") is True
assert config.should_retrieve("can you help?") is True
def test_policy_auto_short_queries(self):
"""Test AUTO policy skips short queries."""
config = RetrievalConfig(policy=RetrievalPolicy.AUTO, auto_min_length=10)
assert config.should_retrieve("hi") is False
assert config.should_retrieve("hello") is False
assert config.should_retrieve("ok") is False
def test_force_retrieval(self):
"""Test force_retrieval overrides policy."""
config = RetrievalConfig(policy=RetrievalPolicy.NEVER)
assert config.should_retrieve("hi", force=True) is True
def test_skip_retrieval(self):
"""Test skip_retrieval overrides policy."""
config = RetrievalConfig(policy=RetrievalPolicy.ALWAYS)
assert config.should_retrieve("what is the answer?", skip=True) is False
def test_disabled_config(self):
"""Test disabled config never retrieves."""
config = RetrievalConfig(enabled=False, policy=RetrievalPolicy.ALWAYS)
assert config.should_retrieve("what is the answer?") is False
assert config.should_retrieve("what is the answer?", force=True) is False
class TestConfigConversion:
"""Test conversion to knowledge_config and rag_config."""
def test_to_knowledge_config(self):
"""Test conversion to Knowledge-compatible config."""
config = RetrievalConfig(
vector_store_provider="chroma",
persist_path=".praison/test",
collection_name="test_collection",
)
knowledge_config = config.to_knowledge_config()
assert knowledge_config["vector_store"]["provider"] == "chroma"
assert knowledge_config["vector_store"]["config"]["path"] == ".praison/test"
assert knowledge_config["vector_store"]["config"]["collection_name"] == "test_collection"
def test_to_knowledge_config_with_rerank(self):
"""Test rerank is included in knowledge config."""
config = RetrievalConfig(rerank=True)
knowledge_config = config.to_knowledge_config()
assert "reranker" in knowledge_config
assert knowledge_config["reranker"]["enabled"] is True
def test_to_rag_config(self):
"""Test conversion to RAG pipeline config."""
config = RetrievalConfig(
top_k=10,
min_score=0.5,
max_context_tokens=8000,
citations=True,
hybrid=True,
rerank=True,
)
rag_config = config.to_rag_config()
assert rag_config["top_k"] == 10
assert rag_config["min_score"] == 0.5
assert rag_config["max_context_tokens"] == 8000
assert rag_config["include_citations"] is True
assert rag_config["rerank"] is True
class TestLegacyConfigMigration:
"""Test migration from legacy knowledge_config and rag_config."""
def test_create_from_knowledge_config(self):
"""Test creation from legacy knowledge_config."""
knowledge_config = {
"vector_store": {
"provider": "mongodb",
"config": {
"collection_name": "my_collection",
"path": ".praison/custom",
}
}
}
config = create_retrieval_config(knowledge_config=knowledge_config)
assert config.vector_store_provider == "mongodb"
assert config.collection_name == "my_collection"
assert config.persist_path == ".praison/custom"
def test_create_from_rag_config(self):
"""Test creation from legacy rag_config."""
rag_config = {
"top_k": 10,
"min_score": 0.3,
"max_context_tokens": 6000,
"include_citations": True,
"rerank": True,
"retrieval_strategy": "hybrid",
}
config = create_retrieval_config(rag_config=rag_config)
assert config.top_k == 10
assert config.min_score == 0.3
assert config.max_context_tokens == 6000
assert config.citations is True
assert config.rerank is True
assert config.hybrid is True
def test_create_from_merged_configs(self):
"""Test creation from both legacy configs merged."""
knowledge_config = {
"vector_store": {
"provider": "chroma",
"config": {"collection_name": "merged"}
}
}
rag_config = {
"top_k": 15,
"rerank": True,
}
config = create_retrieval_config(
knowledge_config=knowledge_config,
rag_config=rag_config,
)
assert config.collection_name == "merged"
assert config.top_k == 15
assert config.rerank is True
def test_create_from_retrieval_config_passthrough(self):
"""Test that RetrievalConfig instance is passed through."""
original = RetrievalConfig(top_k=20)
result = create_retrieval_config(retrieval_config=original)
assert result is original
def test_create_from_retrieval_config_dict(self):
"""Test creation from retrieval_config dict."""
config_dict = {
"top_k": 25,
"policy": "always",
"citations_mode": "inline",
}
config = create_retrieval_config(retrieval_config=config_dict)
assert config.top_k == 25
assert config.policy == RetrievalPolicy.ALWAYS
assert config.citations_mode == CitationsMode.INLINE
class TestSerialization:
"""Test serialization and deserialization."""
def test_to_dict(self):
"""Test conversion to dictionary."""
config = RetrievalConfig(
policy=RetrievalPolicy.ALWAYS,
top_k=10,
citations_mode=CitationsMode.INLINE,
)
data = config.to_dict()
assert data["policy"] == "always"
assert data["top_k"] == 10
assert data["citations_mode"] == "inline"
def test_from_dict(self):
"""Test creation from dictionary."""
data = {
"policy": "never",
"top_k": 15,
"citations_mode": "hidden",
"rerank": True,
}
config = RetrievalConfig.from_dict(data)
assert config.policy == RetrievalPolicy.NEVER
assert config.top_k == 15
assert config.citations_mode == CitationsMode.HIDDEN
assert config.rerank is True
def test_roundtrip(self):
"""Test serialization roundtrip."""
original = RetrievalConfig(
policy=RetrievalPolicy.ALWAYS,
top_k=20,
min_score=0.7,
hybrid=True,
citations_mode=CitationsMode.INLINE,
)
data = original.to_dict()
restored = RetrievalConfig.from_dict(data)
assert restored.policy == original.policy
assert restored.top_k == original.top_k
assert restored.min_score == original.min_score
assert restored.hybrid == original.hybrid
assert restored.citations_mode == original.citations_mode
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.7.0"
version = "0.7.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.2.0.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.2.0.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.2.1.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.2.1.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+6 -2
View File
@@ -191,6 +191,7 @@ def register_commands():
from .commands.tools import app as tools_app
from .commands.knowledge import app as knowledge_app
from .commands.rag import app as rag_app
from .commands import retrieval as retrieval_module
from .commands.deploy import app as deploy_app
from .commands.agents import app as agents_app
from .commands.skills import app as skills_app
@@ -241,8 +242,11 @@ def register_commands():
app.add_typer(memory_app, name="memory", help="Memory management")
app.add_typer(workflow_app, name="workflow", help="Workflow management")
app.add_typer(tools_app, name="tools", help="Tool management")
app.add_typer(knowledge_app, name="knowledge", help="Knowledge base management")
app.add_typer(rag_app, name="rag", help="RAG (Retrieval Augmented Generation)")
app.add_typer(knowledge_app, name="knowledge", help="Knowledge base management (legacy)")
app.add_typer(rag_app, name="rag", help="RAG commands (legacy - use index/query instead)")
# Register unified retrieval commands (Agent-first)
retrieval_module.register_commands(app)
app.add_typer(deploy_app, name="deploy", help="Deployment management")
app.add_typer(agents_app, name="agents", help="Agent management")
app.add_typer(skills_app, name="skills", help="Skill management")
@@ -0,0 +1,377 @@
"""
Unified Retrieval CLI Commands - Agent-First Experience
This module provides the unified CLI commands for knowledge and retrieval:
- `praisonai index` - Index documents into knowledge base
- `praisonai query` - Query with structured answer and citations
- `praisonai chat` - Conversational interface with knowledge (handled by chat.py)
These commands replace the separate `knowledge` and `rag` command families.
"""
import typer
from pathlib import Path
from typing import List, Optional
from contextlib import contextmanager
import time
import sys
app = typer.Typer(
name="retrieval",
help="Knowledge indexing and retrieval commands",
no_args_is_help=True,
)
@contextmanager
def retrieval_profiler(profile: bool, profile_out: Optional[Path], profile_top: int):
"""Context manager for profiling retrieval operations."""
if not profile:
yield None
return
import json as json_module
start_time = time.perf_counter()
start_modules = set(sys.modules.keys())
profile_data = {
"metrics": {},
"modules_loaded": [],
}
try:
import tracemalloc
tracemalloc.start()
has_tracemalloc = True
except ImportError:
has_tracemalloc = False
yield profile_data
elapsed_ms = (time.perf_counter() - start_time) * 1000
profile_data["metrics"]["elapsed_ms"] = elapsed_ms
if has_tracemalloc:
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
profile_data["metrics"]["peak_memory_mb"] = peak / 1024 / 1024
new_modules = set(sys.modules.keys()) - start_modules
profile_data["modules_loaded"] = sorted(list(new_modules))[:profile_top]
if profile_out:
try:
profile_out.parent.mkdir(parents=True, exist_ok=True)
with open(profile_out, "w") as f:
json_module.dump(profile_data, f, indent=2, default=str)
except Exception as e:
print(f"Warning: Failed to save profile: {e}")
from rich.console import Console
console = Console(stderr=True)
console.print(f"\n[dim]Profile: {elapsed_ms:.2f}ms wall time[/dim]")
if "peak_memory_mb" in profile_data["metrics"]:
console.print(f"[dim]Profile: {profile_data['metrics']['peak_memory_mb']:.2f}MB peak memory[/dim]")
console.print(f"[dim]Profile: {len(new_modules)} modules imported[/dim]")
if profile_out:
console.print(f"[dim]Profile saved to: {profile_out}[/dim]")
@app.command("index")
def index_command(
sources: List[str] = typer.Argument(..., help="Source files, directories, or URLs to index"),
collection: str = typer.Option("default", "--collection", "-c", help="Collection/knowledge base name"),
config: Optional[Path] = typer.Option(None, "--config", "-f", help="Config file path"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
profile: bool = typer.Option(False, "--profile", help="Enable performance profiling"),
profile_out: Optional[Path] = typer.Option(None, "--profile-out", help="Save profile to JSON file"),
profile_top: int = typer.Option(20, "--profile-top", help="Top N items in profile"),
):
"""
Index documents into a knowledge base.
This is the canonical command for adding documents to a knowledge base.
Use `praisonai query` to get answers with citations from the indexed knowledge.
Examples:
praisonai index ./docs
praisonai index paper.pdf --collection research
praisonai index ./data --profile --profile-out ./profile.json
"""
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
with retrieval_profiler(profile, profile_out, profile_top) as profile_data:
try:
from praisonaiagents.knowledge import Knowledge
knowledge_config = {
"vector_store": {
"provider": "chroma",
"config": {
"collection_name": collection,
"path": f"./.praison/knowledge/{collection}",
}
}
}
if config and config.exists():
import yaml
with open(config) as f:
file_config = yaml.safe_load(f)
if "knowledge" in file_config:
knowledge_config.update(file_config["knowledge"])
knowledge = Knowledge(config=knowledge_config, verbose=verbose)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
transient=True,
) as progress:
for source in sources:
task = progress.add_task(f"Indexing {source}...", total=None)
try:
result = knowledge.add(source)
count = len(result.get("results", [])) if isinstance(result, dict) else 0
console.print(f"[green]✓[/green] Indexed {source}: {count} chunks")
except Exception as e:
console.print(f"[red]✗[/red] Failed to index {source}: {e}")
progress.remove_task(task)
console.print(f"\n[bold green]Indexing complete![/bold green] Collection: {collection}")
if profile_data:
profile_data["command"] = "index"
profile_data["collection"] = collection
profile_data["sources"] = sources
except ImportError as e:
console.print(f"[red]Error:[/red] Missing dependency: {e}")
console.print("Install with: pip install 'praisonaiagents[knowledge]'")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
@app.command("query")
def query_command(
question: str = typer.Argument(..., help="Question to answer"),
collection: str = typer.Option("default", "--collection", "-c", help="Collection to query"),
top_k: int = typer.Option(5, "--top-k", "-k", help="Number of results to retrieve"),
min_score: float = typer.Option(0.0, "--min-score", help="Minimum relevance score (0.0-1.0)"),
hybrid: bool = typer.Option(False, "--hybrid", help="Use hybrid retrieval (dense + keyword)"),
rerank: bool = typer.Option(False, "--rerank", help="Enable reranking of results"),
citations: bool = typer.Option(True, "--citations/--no-citations", help="Include citations"),
citations_mode: str = typer.Option("append", "--citations-mode", help="Citations mode: append, inline, hidden"),
max_context_tokens: int = typer.Option(4000, "--max-context-tokens", help="Maximum context tokens"),
config: Optional[Path] = typer.Option(None, "--config", "-f", help="Config file path"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
profile: bool = typer.Option(False, "--profile", help="Enable performance profiling"),
profile_out: Optional[Path] = typer.Option(None, "--profile-out", help="Save profile to JSON file"),
profile_top: int = typer.Option(20, "--profile-top", help="Top N items in profile"),
):
"""
Query knowledge and get a structured answer with citations.
Uses the Agent-first retrieval pipeline with token-aware context building.
Examples:
praisonai query "What is the main finding?"
praisonai query "Summarize the document" --collection research
praisonai query "Key points?" --top-k 10 --no-citations
praisonai query "Summary?" --hybrid --rerank
"""
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
console = Console()
with retrieval_profiler(profile, profile_out, profile_top) as profile_data:
try:
from praisonaiagents import Agent
from praisonaiagents.rag.retrieval_config import RetrievalConfig, RetrievalPolicy, CitationsMode
retrieval_config = RetrievalConfig(
enabled=True,
policy=RetrievalPolicy.ALWAYS,
top_k=top_k,
min_score=min_score,
max_context_tokens=max_context_tokens,
rerank=rerank,
hybrid=hybrid,
citations=citations,
citations_mode=CitationsMode(citations_mode),
vector_store_provider="chroma",
persist_path=f"./.praison/knowledge/{collection}",
collection_name=collection,
)
if config and config.exists():
import yaml
with open(config) as f:
file_config = yaml.safe_load(f)
if "retrieval" in file_config:
for key, value in file_config["retrieval"].items():
if hasattr(retrieval_config, key):
setattr(retrieval_config, key, value)
agent = Agent(
name="QueryAgent",
instructions="You are a helpful assistant that answers questions based on the provided knowledge.",
knowledge=[], # Empty - we'll use existing indexed knowledge
retrieval_config=retrieval_config,
verbose=verbose,
)
agent._ensure_knowledge_processed = lambda: None
from praisonaiagents.knowledge import Knowledge
agent.knowledge = Knowledge(config=retrieval_config.to_knowledge_config(), verbose=verbose)
agent._knowledge_processed = True
if verbose:
strategy_str = "hybrid (dense + keyword)" if hybrid else "dense"
rerank_str = " with reranking" if rerank else ""
console.print(f"[dim]Querying collection '{collection}' using {strategy_str}{rerank_str}, top_k={top_k}...[/dim]")
result = agent.query(question)
console.print(Panel(
Markdown(result.answer),
title="[bold]Answer[/bold]",
border_style="green",
))
if citations and result.citations:
console.print("\n[bold]Sources:[/bold]")
for citation in result.citations:
score_str = f"[dim](score: {citation.score:.2f})[/dim]" if citation.score else ""
console.print(f" [{citation.id}] {citation.source} {score_str}")
if verbose:
snippet = citation.text[:200] + "..." if len(citation.text) > 200 else citation.text
console.print(f" [dim]{snippet}[/dim]")
if verbose and result.metadata:
elapsed = result.metadata.get('elapsed_seconds', 0)
console.print(f"\n[dim]Elapsed: {elapsed:.2f}s[/dim]")
if profile_data:
profile_data["command"] = "query"
profile_data["collection"] = collection
profile_data["question"] = question
except ImportError as e:
console.print(f"[red]Error:[/red] Missing dependency: {e}")
console.print("Install with: pip install 'praisonaiagents[knowledge]'")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
if verbose:
import traceback
console.print(traceback.format_exc())
raise typer.Exit(1)
@app.command("search")
def search_command(
query: str = typer.Argument(..., help="Search query"),
collection: str = typer.Option("default", "--collection", "-c", help="Collection to search"),
top_k: int = typer.Option(5, "--top-k", "-k", help="Number of results to retrieve"),
hybrid: bool = typer.Option(False, "--hybrid", help="Use hybrid retrieval (dense + keyword)"),
config: Optional[Path] = typer.Option(None, "--config", "-f", help="Config file path"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
):
"""
Search/retrieve from a knowledge base (no LLM generation).
Returns raw search results without generating an answer.
For answers with citations, use `praisonai query`.
Examples:
praisonai search "capital of France"
praisonai search "main findings" --collection research --top-k 10
"""
from rich.console import Console
from rich.table import Table
console = Console()
try:
from praisonaiagents.knowledge import Knowledge
knowledge_config = {
"vector_store": {
"provider": "chroma",
"config": {
"collection_name": collection,
"path": f"./.praison/knowledge/{collection}",
}
}
}
if hybrid:
knowledge_config["retrieval"] = {"strategy": "hybrid"}
if config and config.exists():
import yaml
with open(config) as f:
file_config = yaml.safe_load(f)
if "knowledge" in file_config:
knowledge_config.update(file_config["knowledge"])
knowledge = Knowledge(config=knowledge_config, verbose=verbose)
if verbose:
console.print(f"[dim]Searching collection '{collection}' for: {query}[/dim]")
results = knowledge.search(query)
if not results:
console.print("[yellow]No results found.[/yellow]")
return
if isinstance(results, dict) and 'results' in results:
result_list = results['results']
elif isinstance(results, list):
result_list = results
else:
result_list = [results]
table = Table(title=f"Search Results ({len(result_list[:top_k])} of {len(result_list)})")
table.add_column("#", style="dim", width=3)
table.add_column("Score", width=8)
table.add_column("Content", overflow="fold")
for i, result in enumerate(result_list[:top_k], 1):
if isinstance(result, dict):
content = result.get('memory', result.get('text', str(result)))
score = result.get('score', result.get('distance', 'N/A'))
else:
content = str(result)
score = 'N/A'
content_preview = content[:300] + "..." if len(content) > 300 else content
score_str = f"{score:.3f}" if isinstance(score, float) else str(score)
table.add_row(str(i), score_str, content_preview)
console.print(table)
except ImportError as e:
console.print(f"[red]Error:[/red] Missing dependency: {e}")
console.print("Install with: pip install 'praisonaiagents[knowledge]'")
raise typer.Exit(1)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
def register_commands(main_app: typer.Typer):
"""Register unified retrieval commands with the main CLI app."""
main_app.command("index")(index_command)
main_app.command("query")(query_command)
main_app.command("search")(search_command)
+38
View File
@@ -898,6 +898,30 @@ class PraisonAI:
parser.add_argument("--compare", type=str, help="Compare CLI modes (comma-separated: basic,tools,research,planning)")
parser.add_argument("--compare-output", type=str, help="Save comparison results to file")
# Context Management - context budgeting, optimization, and monitoring
parser.add_argument("--context-auto-compact", action="store_true", dest="context_auto_compact",
help="Enable automatic context compaction (default in interactive mode)")
parser.add_argument("--no-context-auto-compact", action="store_false", dest="context_auto_compact",
help="Disable automatic context compaction")
parser.add_argument("--context-strategy", type=str, choices=["truncate", "sliding_window", "prune_tools", "summarize", "smart"],
help="Context optimization strategy (default: smart)")
parser.add_argument("--context-threshold", type=float, metavar="0.0-1.0",
help="Trigger compaction at this utilization (default: 0.8)")
parser.add_argument("--context-monitor", action="store_true", dest="context_monitor",
help="Enable real-time context monitoring (writes to context.txt)")
parser.add_argument("--context-monitor-path", type=str, metavar="PATH",
help="Path for context monitor output (default: ./context.txt)")
parser.add_argument("--context-monitor-format", type=str, choices=["human", "json"],
help="Context monitor output format (default: human)")
parser.add_argument("--context-monitor-frequency", type=str, choices=["turn", "tool_call", "manual", "overflow"],
help="Context monitor update frequency (default: turn)")
parser.add_argument("--context-redact", action="store_true", dest="context_redact",
help="Redact sensitive data in context monitor output (default: true)")
parser.add_argument("--no-context-redact", action="store_false", dest="context_redact",
help="Disable sensitive data redaction in context monitor")
parser.add_argument("--context-output-reserve", type=int, metavar="TOKENS",
help="Reserve tokens for model output (default: 8000)")
# Agent Scheduler - for schedule command
parser.add_argument("--interval", dest="schedule_interval", type=str, help="Schedule interval (e.g., 'hourly', '*/30m', 'daily')")
parser.add_argument("--schedule-max-retries", dest="schedule_max_retries", type=int, help="Maximum retry attempts for scheduled execution")
@@ -4935,6 +4959,20 @@ Now, {final_instruction.lower()}:"""
'session_store': session_store, # Reference to store for saving
}
# Initialize context manager with CLI flags
context_config = {
'auto_compact': getattr(args, 'context_auto_compact', True), # Default True in interactive
'strategy': getattr(args, 'context_strategy', 'smart'),
'threshold': getattr(args, 'context_threshold', 0.8),
'monitor_enabled': getattr(args, 'context_monitor', False),
'monitor_path': getattr(args, 'context_monitor_path', './context.txt'),
'monitor_format': getattr(args, 'context_monitor_format', 'human'),
'monitor_frequency': getattr(args, 'context_monitor_frequency', 'turn'),
'redact_sensitive': getattr(args, 'context_redact', True),
'output_reserve': getattr(args, 'context_output_reserve', 8000),
}
session_state['context_config'] = context_config
# Start the execution worker thread (TRUE ASYNC - runs in background)
worker_thread = self._start_execution_worker(tools_list, console, session_state)
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==3.2.0 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==3.2.1 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.2.0"
__version__ = "3.2.1"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.7.0",
"praisonaiagents>=0.7.1",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
+4 -4
View File
@@ -4276,7 +4276,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
{ name = "praisonaiagents", specifier = ">=0.7.0" },
{ name = "praisonaiagents", specifier = ">=0.7.1" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4324,7 +4324,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.7.0"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4333,9 +4333,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/14/cb92a1e8ab1e210e78338bfed30d617ad10ad96452a0b0bed85dacf992eb/praisonaiagents-0.7.0.tar.gz", hash = "sha256:e393749cc9da921e98cf47843675066f60adc9fa8ba2a0748add77a1e2effa8d", size = 861098, upload-time = "2026-01-07T19:36:49.455Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/47/a5630fa4e12e0d28685ed7dd846e41720ce441ea388363c69710f7c86de4/praisonaiagents-0.7.1.tar.gz", hash = "sha256:9458d9b15c404e8efb44c20c0a5088587d7f9b77c23d2fa3b48262a1e01d6110", size = 865614, upload-time = "2026-01-07T21:50:12.162Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/bf/ce802a9f899131c8ff525b669f3754cf23d39a8e612a2aa1d18401a3569a/praisonaiagents-0.7.0-py3-none-any.whl", hash = "sha256:a1cd9514e95dc54cd214548a72c734ba89131e58bb2b6a34c43f0e1ac36e72eb", size = 822619, upload-time = "2026-01-07T19:36:47.174Z" },
{ url = "https://files.pythonhosted.org/packages/58/b3/176f12c97887e2a7872104f2acf89508db4d8ea28d7555c470e8617add81/praisonaiagents-0.7.1-py3-none-any.whl", hash = "sha256:e1d670f7b2298127292af8a7ccfdc7a942a8610dc3bd55110d9497fe6114862e", size = 827576, upload-time = "2026-01-07T21:50:09.998Z" },
]
[[package]]