mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
412783c5ed |
No files matched your search
@@ -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]"
|
||||
|
||||
|
||||
@@ -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]" \
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "3.2.1"
|
||||
@@ -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",
|
||||
|
||||
Generated
+4
-4
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user