forked from cleveragents/cleveragents-core
fix: thread safety AgentWithMemory and improve ToolAgent JSON parsing and mypy issues
This commit is contained in:
@@ -36,7 +36,7 @@ pip install cleveragents
|
||||
|
||||
```bash
|
||||
git clone https://git.cleverthis.com/cleveragents/cleveragents-core
|
||||
cd cleveragents
|
||||
cd cleveragents-core
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
@@ -272,7 +272,7 @@ cleveragents interactive -c examples/collaboration_reactive.yaml
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
# Run all BDD tests
|
||||
python -m behave tests/features
|
||||
|
||||
# Run with coverage
|
||||
@@ -294,12 +294,11 @@ tox -e py312-cover
|
||||
tox -e py312-nocov
|
||||
|
||||
# Individual test scripts:
|
||||
bash tests/scripts/test_single_turn_writer.sh
|
||||
bash tests/scripts/test_multi_agent_paper_writer_langgraph.sh
|
||||
bash tests/scripts/test_legal_contract_analyzer_langgraph.sh
|
||||
bash tests/scripts/test_paper_writer_section_by_section.sh
|
||||
bash tests/scripts/test_multi_agent_interactive.sh
|
||||
bash tests/scripts/test_legal_contract_analyzer.sh
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
# Legal Contract Metadata Extractor - True Multi-Agent Analysis System
|
||||
# Multiple specialized agents actually working together through stream pipeline
|
||||
#
|
||||
# AGENTS:
|
||||
# - Coordinator: Routes workflow and loads documents
|
||||
# - Text Preprocessor: Cleans and structures contract text
|
||||
# - Contract Analyzer: Extracts detailed metadata
|
||||
# - Risk Assessor: Identifies risks and issues
|
||||
# - JSON Formatter: Creates structured output
|
||||
# - File Manager: Saves analysis results
|
||||
#
|
||||
# USAGE:
|
||||
# cleveragents interactive -c examples/legal_contract_metadata_extractor.yaml --unsafe
|
||||
#
|
||||
# HOW IT WORKS:
|
||||
# - Each agent actually executes (not simulation)
|
||||
# - Messages flow through stream pipeline: coordinator → preprocessor → analyzer → risk_assessor → formatter
|
||||
# - All agents share conversation history (memory enabled)
|
||||
# - Natural progression through analysis stages
|
||||
|
||||
agents:
|
||||
# File operations agents
|
||||
file_reader:
|
||||
type: tool
|
||||
config:
|
||||
tools: ["file_read"]
|
||||
safe_mode: true
|
||||
|
||||
file_manager:
|
||||
type: tool
|
||||
config:
|
||||
tools: ["file_write"]
|
||||
safe_mode: false
|
||||
|
||||
# Coordinator agent - manages workflow and loads documents
|
||||
coordinator:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
temperature: 0.3
|
||||
memory_enabled: true
|
||||
max_history: 20
|
||||
max_tokens: 2000
|
||||
system_prompt: |
|
||||
You are the COORDINATOR agent in a multi-agent legal contract analysis system.
|
||||
|
||||
YOUR ROLE: Load contract documents and initiate the analysis workflow.
|
||||
|
||||
RESPONSIBILITIES:
|
||||
- Detect when user provides a file path
|
||||
- Load the contract document using file_read tool
|
||||
- Pass the loaded content to the TEXT_PREPROCESSOR agent
|
||||
|
||||
FILE PATH DETECTION:
|
||||
- Look for file paths in user input (e.g., "sample_contract.txt", "contract.pdf")
|
||||
- Extract the file name from phrases like "analyze sample_contract.txt"
|
||||
- If input contains file extension (.txt, .pdf, .docx) or mentions "contract"
|
||||
|
||||
FILE LOADING:
|
||||
When file path detected, use this exact format:
|
||||
[TOOL_EXECUTE:file_read]
|
||||
{"file": "extracted_file_path"}
|
||||
[/TOOL_EXECUTE]
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"=== COORDINATOR AGENT ===>
|
||||
Loading contract document from [filename]...
|
||||
|
||||
[Tool execution will happen automatically]
|
||||
|
||||
Contract loaded. Passing to TEXT_PREPROCESSOR for cleaning and structuring."
|
||||
|
||||
Always start responses with: [COORDINATOR SPEAKING]
|
||||
|
||||
# Text preprocessing agent - cleans and structures contract text
|
||||
text_preprocessor:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-3.5-turbo
|
||||
temperature: 0.1
|
||||
memory_enabled: true
|
||||
max_history: 20
|
||||
system_prompt: |
|
||||
You are the TEXT_PREPROCESSOR agent in a legal contract analysis system.
|
||||
|
||||
YOUR ROLE: Clean and structure raw contract text for analysis.
|
||||
|
||||
RESPONSIBILITIES:
|
||||
- Fix OCR errors and formatting issues
|
||||
- Identify document sections (headers, clauses, signatures)
|
||||
- Normalize dates to standard format (YYYY-MM-DD)
|
||||
- Standardize monetary amounts ($X,XXX.XX USD)
|
||||
- Extract proper nouns (companies, people, locations)
|
||||
- Label key sections with clear markers
|
||||
|
||||
SECTION MARKERS:
|
||||
[PARTIES] ... [/PARTIES]
|
||||
[DATES] ... [/DATES]
|
||||
[FINANCIAL_TERMS] ... [/FINANCIAL_TERMS]
|
||||
[OBLIGATIONS] ... [/OBLIGATIONS]
|
||||
[LEGAL_TERMS] ... [/LEGAL_TERMS]
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"=== TEXT_PREPROCESSOR AGENT ===>
|
||||
Cleaning and structuring the contract text...
|
||||
|
||||
[PARTIES]
|
||||
- TechCorp Solutions Inc. (Provider)
|
||||
- Global Enterprises LLC (Client)
|
||||
[/PARTIES]
|
||||
|
||||
[DATES]
|
||||
- Effective Date: 2024-01-15
|
||||
- Term: 24 months
|
||||
- Expiration: 2026-01-15
|
||||
[/DATES]
|
||||
|
||||
[FINANCIAL_TERMS]
|
||||
- Monthly Fee: $5,000.00 USD
|
||||
- Payment Due: 30 days from invoice
|
||||
- Late Penalty: 1.5% per month
|
||||
[/FINANCIAL_TERMS]
|
||||
|
||||
[OBLIGATIONS]
|
||||
- Virtual server hosting
|
||||
- Data storage and backup
|
||||
- Network infrastructure management
|
||||
- 24/7 technical support
|
||||
- 99.9% uptime guarantee
|
||||
[/OBLIGATIONS]
|
||||
|
||||
[LEGAL_TERMS]
|
||||
- Governing Law: California
|
||||
- Termination: 90 days notice
|
||||
- Liability Limit: 12 months payments
|
||||
- Confidentiality: Required
|
||||
[/LEGAL_TERMS]
|
||||
|
||||
Text preprocessing complete. Passing to CONTRACT_ANALYZER."
|
||||
|
||||
Always start responses with: [TEXT_PREPROCESSOR SPEAKING]
|
||||
|
||||
# Contract analysis agent - extracts detailed metadata
|
||||
contract_analyzer:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.2
|
||||
memory_enabled: true
|
||||
max_history: 20
|
||||
max_tokens: 2000
|
||||
system_prompt: |
|
||||
You are the CONTRACT_ANALYZER agent in a legal contract analysis system.
|
||||
|
||||
YOUR ROLE: Extract comprehensive metadata from preprocessed contract text.
|
||||
|
||||
EXTRACTION CATEGORIES:
|
||||
- PARTIES: Company names, roles, addresses, representatives
|
||||
- DATES: Signing, effective, expiration, payment, milestones
|
||||
- FINANCIAL TERMS: Values, schedules, penalties, currency
|
||||
- OBLIGATIONS: Deliverables, requirements, standards
|
||||
- LEGAL TERMS: Governing law, termination, liability, IP, confidentiality
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"=== CONTRACT_ANALYZER AGENT ===>
|
||||
Extracting comprehensive metadata from the contract...
|
||||
|
||||
PARTIES INFORMATION:
|
||||
- Provider: TechCorp Solutions Inc.
|
||||
* Type: Delaware Corporation
|
||||
* Address: 123 Tech Street, San Francisco, CA 94105
|
||||
* Representative: John Smith (CTO)
|
||||
* Confidence: 0.95
|
||||
|
||||
- Client: Global Enterprises LLC
|
||||
* Type: California LLC
|
||||
* Address: 456 Business Ave, Los Angeles, CA 90001
|
||||
* Representative: Sarah Johnson (CEO)
|
||||
* Confidence: 0.95
|
||||
|
||||
KEY DATES:
|
||||
- Effective Date: January 15, 2024 (Confidence: 1.0)
|
||||
- Contract Term: 24 months (Confidence: 1.0)
|
||||
- Termination Notice: 90 days (Confidence: 1.0)
|
||||
- Payment Due: 30 days from invoice (Confidence: 1.0)
|
||||
|
||||
FINANCIAL TERMS:
|
||||
- Monthly Fee: $5,000.00 USD (Confidence: 1.0)
|
||||
- Late Payment Penalty: 1.5% per month (Confidence: 1.0)
|
||||
- Currency: USD (Confidence: 1.0)
|
||||
|
||||
OBLIGATIONS:
|
||||
Provider must deliver:
|
||||
- Virtual server hosting (Confidence: 1.0)
|
||||
- Data storage and backup (Confidence: 1.0)
|
||||
- Network infrastructure management (Confidence: 1.0)
|
||||
- 24/7 technical support (Confidence: 1.0)
|
||||
- 99.9% uptime guarantee (Confidence: 1.0)
|
||||
|
||||
LEGAL TERMS:
|
||||
- Governing Law: State of California (Confidence: 1.0)
|
||||
- Termination Rights: 90 days notice or immediate for breach (Confidence: 1.0)
|
||||
- Liability Limit: 12 months of payments (Confidence: 1.0)
|
||||
- Confidentiality: Required for proprietary information (Confidence: 1.0)
|
||||
|
||||
Metadata extraction complete. Passing to RISK_ASSESSOR."
|
||||
|
||||
Always start responses with: [CONTRACT_ANALYZER SPEAKING]
|
||||
|
||||
# Risk assessment agent - identifies issues and risks
|
||||
risk_assessor:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
temperature: 0.3
|
||||
memory_enabled: true
|
||||
max_history: 20
|
||||
max_tokens: 2000
|
||||
system_prompt: |
|
||||
You are the RISK_ASSESSOR agent in a legal contract analysis system.
|
||||
|
||||
YOUR ROLE: Identify risks, issues, and potential problems in contracts.
|
||||
|
||||
RISK ASSESSMENT AREAS:
|
||||
- HIGH-RISK TERMS: Unusual or unfavorable provisions
|
||||
- MISSING CLAUSES: Absent standard protections
|
||||
- AMBIGUOUS LANGUAGE: Unclear or vague terms
|
||||
- COMPLIANCE CONCERNS: Regulatory or industry issues
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"=== RISK_ASSESSOR AGENT ===>
|
||||
Conducting comprehensive risk assessment...
|
||||
|
||||
HIGH-RISK TERMS IDENTIFIED:
|
||||
- Limited liability cap may be insufficient for data breach scenarios
|
||||
- No specific data privacy or GDPR compliance mentioned
|
||||
- Confidence: 0.85
|
||||
|
||||
MISSING STANDARD CLAUSES:
|
||||
- Force Majeure provisions (Confidence: 0.95)
|
||||
- Dispute Resolution mechanism (Confidence: 0.90)
|
||||
- Data protection and privacy clauses (Confidence: 0.95)
|
||||
- Intellectual Property ownership clarification (Confidence: 0.85)
|
||||
- Service Level Agreement penalties for downtime (Confidence: 0.80)
|
||||
|
||||
AMBIGUOUS LANGUAGE:
|
||||
- 'Proprietary information' not clearly defined (Confidence: 0.85)
|
||||
- Technical specifications for services not detailed (Confidence: 0.80)
|
||||
|
||||
COMPLIANCE CONCERNS:
|
||||
- No data breach notification procedures
|
||||
- Missing data residency requirements
|
||||
- No mention of industry-specific compliance (HIPAA, SOC2, etc.)
|
||||
|
||||
OVERALL RISK ASSESSMENT:
|
||||
- Risk Score: 0.65 (Medium-High)
|
||||
- Risk Level: Medium-High
|
||||
- Recommendation: Add data protection clauses and dispute resolution
|
||||
|
||||
Risk assessment complete. Passing to JSON_FORMATTER."
|
||||
|
||||
Always start responses with: [RISK_ASSESSOR SPEAKING]
|
||||
|
||||
# JSON formatting agent - structures analysis for programmatic use
|
||||
metadata_formatter:
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-3.5-turbo
|
||||
temperature: 0.0
|
||||
memory_enabled: true
|
||||
max_history: 15
|
||||
max_tokens: 3000
|
||||
system_prompt: |
|
||||
You are the JSON_FORMATTER agent in a legal contract analysis system.
|
||||
|
||||
YOUR ROLE: Convert contract analysis into structured JSON format.
|
||||
|
||||
TASK:
|
||||
- Review the conversation history
|
||||
- Extract all analysis from TEXT_PREPROCESSOR, CONTRACT_ANALYZER, and RISK_ASSESSOR
|
||||
- Format into comprehensive JSON structure
|
||||
|
||||
JSON OUTPUT STRUCTURE:
|
||||
{
|
||||
"document_info": {
|
||||
"analysis_date": "YYYY-MM-DD",
|
||||
"document_type": "service_agreement",
|
||||
"total_confidence": 0.91,
|
||||
"analysis_version": "1.0"
|
||||
},
|
||||
"parties": [...],
|
||||
"dates": {...},
|
||||
"financial_terms": {...},
|
||||
"obligations": {...},
|
||||
"legal_terms": {...},
|
||||
"risk_assessment": {...},
|
||||
"extraction_summary": {...}
|
||||
}
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"=== JSON_FORMATTER AGENT ===>
|
||||
Converting analysis to structured JSON format...
|
||||
|
||||
```json
|
||||
{
|
||||
[COMPLETE JSON WITH ALL EXTRACTED DATA]
|
||||
}
|
||||
```
|
||||
|
||||
Analysis complete. To save results, ask user for filename."
|
||||
|
||||
Always start responses with: [JSON_FORMATTER SPEAKING]
|
||||
|
||||
routes:
|
||||
# Stage 1: Coordinator loads document
|
||||
coordinator_stream:
|
||||
type: stream
|
||||
stream_type: cold
|
||||
operators:
|
||||
- type: map
|
||||
params:
|
||||
agent: coordinator
|
||||
|
||||
# Stage 2: Text Preprocessor cleans text
|
||||
preprocessor_stream:
|
||||
type: stream
|
||||
stream_type: cold
|
||||
operators:
|
||||
- type: map
|
||||
params:
|
||||
agent: text_preprocessor
|
||||
|
||||
# Stage 3: Contract Analyzer extracts metadata
|
||||
analyzer_stream:
|
||||
type: stream
|
||||
stream_type: cold
|
||||
operators:
|
||||
- type: map
|
||||
params:
|
||||
agent: contract_analyzer
|
||||
|
||||
# Stage 4: Risk Assessor identifies issues
|
||||
risk_stream:
|
||||
type: stream
|
||||
stream_type: cold
|
||||
operators:
|
||||
- type: map
|
||||
params:
|
||||
agent: risk_assessor
|
||||
|
||||
# Stage 5: JSON Formatter creates structured output
|
||||
formatter_stream:
|
||||
type: stream
|
||||
stream_type: cold
|
||||
operators:
|
||||
- type: map
|
||||
params:
|
||||
agent: metadata_formatter
|
||||
publications:
|
||||
- __output__
|
||||
|
||||
merges:
|
||||
- sources: [__input__]
|
||||
target: coordinator_stream
|
||||
- sources: [coordinator_stream]
|
||||
target: preprocessor_stream
|
||||
- sources: [preprocessor_stream]
|
||||
target: analyzer_stream
|
||||
- sources: [analyzer_stream]
|
||||
target: risk_stream
|
||||
- sources: [risk_stream]
|
||||
target: formatter_stream
|
||||
|
||||
context:
|
||||
global:
|
||||
app_name: "Legal Contract Metadata Extractor"
|
||||
version: "2.0-true-multi-agent"
|
||||
_unsafe_mode: true
|
||||
log_level: "INFO"
|
||||
@@ -44,56 +44,56 @@ agents:
|
||||
max_history: 30
|
||||
system_prompt: |
|
||||
You are the COORDINATOR agent in a multi-agent paper writing system.
|
||||
|
||||
|
||||
YOUR ROLE: Analyze user input and decide which specialist should handle it.
|
||||
|
||||
|
||||
AVAILABLE SPECIALISTS:
|
||||
- RESEARCHER: For gathering requirements, understanding research questions
|
||||
- WRITER: For writing papers (whole or sections)
|
||||
- REVIEWER: For reviewing and improving written content
|
||||
- FILE_MANAGER: For file operations (save, read, append)
|
||||
|
||||
|
||||
ROUTING DECISIONS:
|
||||
Based on conversation context, output ONE of these routing tags:
|
||||
|
||||
|
||||
[ROUTE:RESEARCHER] - If user wants to start, discuss topic, or define requirements
|
||||
[ROUTE:WRITER] - If requirements are clear and user wants content written
|
||||
[ROUTE:REVIEWER] - If paper/content exists and needs review/improvement
|
||||
[ROUTE:FILE_MANAGER] - If user wants to save/read/append content to a file
|
||||
[ROUTE:END] - If workflow is complete or user says goodbye
|
||||
|
||||
|
||||
FILE OPERATION INSTRUCTIONS:
|
||||
When user wants file operations, extract key information and route to FILE_MANAGER:
|
||||
|
||||
|
||||
For SAVE/WRITE (new file or overwrite):
|
||||
"User wants to save content to 'paper.txt' (new/overwrite).
|
||||
[ROUTE:FILE_MANAGER]
|
||||
OPERATION: write
|
||||
FILENAME: paper.txt"
|
||||
|
||||
|
||||
For APPEND (add to end of file):
|
||||
"User wants to append new section to 'paper.txt'.
|
||||
[ROUTE:FILE_MANAGER]
|
||||
OPERATION: append
|
||||
FILENAME: paper.txt"
|
||||
|
||||
|
||||
For READ (read existing file):
|
||||
"User wants to read 'paper.txt'.
|
||||
[ROUTE:FILE_MANAGER]
|
||||
OPERATION: read
|
||||
FILENAME: paper.txt"
|
||||
|
||||
|
||||
SECTION-BY-SECTION WORKFLOW:
|
||||
- User can write one section at a time (abstract, introduction, methodology, etc.)
|
||||
- Each section can be saved individually or appended to existing file
|
||||
- Support iterative refinement of individual sections
|
||||
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Provide brief context, then routing decision:
|
||||
|
||||
|
||||
"User wants to write the abstract for a quantum computing paper. Routing to writer for content creation.
|
||||
[ROUTE:WRITER]"
|
||||
|
||||
|
||||
Always include the [ROUTE:X] tag in your response!
|
||||
|
||||
# Researcher agent - gathers requirements
|
||||
@@ -107,9 +107,9 @@ agents:
|
||||
max_history: 30
|
||||
system_prompt: |
|
||||
You are the RESEARCHER agent in a multi-agent paper writing system.
|
||||
|
||||
|
||||
YOUR ROLE: Gather requirements and define the research scope for papers.
|
||||
|
||||
|
||||
RESPONSIBILITIES:
|
||||
- Ask clarifying questions about the research topic
|
||||
- Understand the research question or thesis
|
||||
@@ -117,22 +117,22 @@ agents:
|
||||
- Define paper scope and key points
|
||||
- Gather any specific requirements
|
||||
- Support both full papers and individual sections
|
||||
|
||||
|
||||
CONVERSATION CONTEXT:
|
||||
- You can see the entire conversation history
|
||||
- Build on what users have already said
|
||||
- Don't repeat questions if information was already provided
|
||||
|
||||
|
||||
COMPLETION SIGNAL:
|
||||
When you have enough information, end your response with:
|
||||
"[REQUIREMENTS_COMPLETE]"
|
||||
|
||||
|
||||
This signals the coordinator to route to the WRITER.
|
||||
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"[RESEARCHER SPEAKING]
|
||||
[Your questions and analysis]
|
||||
|
||||
|
||||
[REQUIREMENTS_COMPLETE] (only when done)"
|
||||
|
||||
# Writer agent - writes papers or sections
|
||||
@@ -147,65 +147,65 @@ agents:
|
||||
max_history: 30
|
||||
system_prompt: |
|
||||
You are the WRITER agent in a multi-agent paper writing system.
|
||||
|
||||
|
||||
YOUR ROLE: Write complete papers OR individual sections based on user request.
|
||||
|
||||
|
||||
ACCESSING REQUIREMENTS:
|
||||
- Review the COMPLETE conversation history
|
||||
- Look for "[RESEARCHER SPEAKING]" for requirements
|
||||
- Look for coordinator instructions about what to write
|
||||
- Check if user wants full paper or specific section
|
||||
|
||||
|
||||
FULL PAPER STRUCTURE:
|
||||
# [Descriptive Title Based on Topic]
|
||||
|
||||
|
||||
## Abstract
|
||||
[150-250 words]
|
||||
|
||||
|
||||
## Introduction
|
||||
[Background and research question]
|
||||
|
||||
|
||||
## Methodology
|
||||
[Research approach]
|
||||
|
||||
|
||||
## Results
|
||||
[Key findings]
|
||||
|
||||
|
||||
## Discussion
|
||||
[Analysis]
|
||||
|
||||
|
||||
## Conclusion
|
||||
[Summary and future work]
|
||||
|
||||
|
||||
## References
|
||||
[3-5 relevant references]
|
||||
|
||||
|
||||
SECTION-BY-SECTION MODE:
|
||||
If user asks for specific section (e.g., "write the abstract", "write introduction"):
|
||||
- Write ONLY that section with appropriate heading
|
||||
- Make it complete and self-contained
|
||||
- Follow academic writing standards
|
||||
- Include section heading (e.g., "## Abstract")
|
||||
|
||||
|
||||
CONTINUING SECTIONS:
|
||||
If user says "now write the next section" or "write methodology":
|
||||
- Review conversation history to see what's already written
|
||||
- Write the requested section that follows logically
|
||||
- Maintain consistent style and tone
|
||||
|
||||
|
||||
WRITING STYLE:
|
||||
- Academic and professional
|
||||
- Appropriate for target audience
|
||||
- Clear and well-structured
|
||||
|
||||
|
||||
COMPLETION SIGNAL:
|
||||
End your response with: "[PAPER_COMPLETE]" for full paper or "[SECTION_COMPLETE]" for individual section
|
||||
|
||||
End your response with: "[PAPER_COMPLETE]" for full paper and ignore the ending if writing a section
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"[WRITER SPEAKING]
|
||||
[Content here]
|
||||
|
||||
[PAPER_COMPLETE] or [SECTION_COMPLETE]"
|
||||
|
||||
[PAPER_COMPLETE] and ignore the ending if writing a section
|
||||
|
||||
# Reviewer agent - reviews and improves papers
|
||||
reviewer:
|
||||
@@ -218,20 +218,20 @@ agents:
|
||||
max_history: 30
|
||||
system_prompt: |
|
||||
You are the REVIEWER agent in a multi-agent paper writing system.
|
||||
|
||||
|
||||
YOUR ROLE: Review papers or sections and provide constructive feedback.
|
||||
|
||||
|
||||
ACCESSING CONTENT TO REVIEW:
|
||||
- Review the COMPLETE conversation history
|
||||
- Look for "[WRITER SPEAKING]" for the content
|
||||
- Look for "[FILE_CONTENT_START]" if content was read from file
|
||||
- Extract the full content (sections or complete paper)
|
||||
- Then provide your detailed review
|
||||
|
||||
|
||||
IMPORTANT: The content EXISTS in the conversation history!
|
||||
Search for "[WRITER SPEAKING]" or "[FILE_CONTENT_START]" and extract everything after it.
|
||||
Do NOT say you can't find the content - it's in the conversation!
|
||||
|
||||
|
||||
REVIEW CRITERIA:
|
||||
- Structure and organization
|
||||
- Clarity and coherence
|
||||
@@ -239,20 +239,20 @@ agents:
|
||||
- Appropriate level for target audience
|
||||
- Completeness
|
||||
- Consistency with previous sections (if reviewing incrementally)
|
||||
|
||||
|
||||
OUTPUT:
|
||||
- Highlight strengths
|
||||
- Identify areas for improvement
|
||||
- Suggest specific changes
|
||||
- Can provide revised sections if needed
|
||||
|
||||
|
||||
COMPLETION SIGNAL:
|
||||
End your response with: "[REVIEW_COMPLETE]"
|
||||
|
||||
|
||||
OUTPUT FORMAT:
|
||||
"[REVIEWER SPEAKING]
|
||||
[Your review and feedback]
|
||||
|
||||
|
||||
[REVIEW_COMPLETE]"
|
||||
|
||||
# File manager agent - intelligently handles file operations
|
||||
@@ -266,39 +266,39 @@ agents:
|
||||
max_history: 30
|
||||
system_prompt: |
|
||||
You are the FILE_MANAGER agent in a multi-agent paper writing system.
|
||||
|
||||
|
||||
YOUR ROLE: Execute file operations based on coordinator instructions.
|
||||
|
||||
|
||||
OPERATIONS:
|
||||
1. WRITE - Save new content or overwrite existing file
|
||||
2. APPEND - Add content to end of existing file
|
||||
3. READ - Read existing file content
|
||||
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Check coordinator message for "OPERATION:" and "FILENAME:"
|
||||
2. Search conversation history for content to save
|
||||
- Look for "[WRITER SPEAKING]" for newly written content
|
||||
- Look for most recent content in conversation
|
||||
3. Format appropriate JSON command based on operation
|
||||
|
||||
|
||||
OUTPUT FORMATS:
|
||||
|
||||
|
||||
For WRITE operation:
|
||||
{"tool": "file_write", "args": {"file": "filename.txt", "content": "CONTENT_HERE", "mode": "w"}}
|
||||
|
||||
|
||||
For APPEND operation:
|
||||
{"tool": "file_write", "args": {"file": "filename.txt", "content": "CONTENT_HERE", "mode": "a"}}
|
||||
|
||||
|
||||
For READ operation:
|
||||
{"tool": "file_read", "args": {"file": "filename.txt"}}
|
||||
|
||||
|
||||
CRITICAL RULES:
|
||||
- Output ONLY the JSON command, no other text before or after
|
||||
- Include the COMPLETE content from writer
|
||||
- Use the exact filename from FILENAME: field
|
||||
- Use correct mode: "w" for write/overwrite, "a" for append
|
||||
- For READ, just read the file (no content needed)
|
||||
|
||||
|
||||
# Tool executor for file reading
|
||||
file_reader_tool:
|
||||
type: tool
|
||||
@@ -319,111 +319,111 @@ routes:
|
||||
type: graph
|
||||
entry_point: start
|
||||
checkpointing: true
|
||||
|
||||
|
||||
nodes:
|
||||
# Coordinator decides routing
|
||||
coordinate:
|
||||
type: agent
|
||||
agent: coordinator
|
||||
|
||||
|
||||
# Researcher gathers requirements
|
||||
research:
|
||||
type: agent
|
||||
agent: researcher
|
||||
|
||||
|
||||
# Writer creates content
|
||||
write:
|
||||
type: agent
|
||||
agent: writer
|
||||
|
||||
|
||||
# Reviewer provides feedback
|
||||
review:
|
||||
type: agent
|
||||
agent: reviewer
|
||||
|
||||
|
||||
# File manager formats file commands
|
||||
manage_file:
|
||||
type: agent
|
||||
agent: file_manager
|
||||
|
||||
|
||||
# Tool executors for file operations
|
||||
read_file:
|
||||
type: agent
|
||||
agent: file_reader_tool
|
||||
|
||||
|
||||
write_file:
|
||||
type: agent
|
||||
agent: file_writer_tool
|
||||
|
||||
|
||||
edges:
|
||||
# Always start with coordinator
|
||||
- source: start
|
||||
target: coordinate
|
||||
|
||||
|
||||
# Coordinator routes to researcher
|
||||
- source: coordinate
|
||||
target: research
|
||||
condition:
|
||||
type: content_contains
|
||||
text: "[ROUTE:RESEARCHER]"
|
||||
|
||||
|
||||
# Coordinator routes to writer
|
||||
- source: coordinate
|
||||
target: write
|
||||
condition:
|
||||
type: content_contains
|
||||
text: "[ROUTE:WRITER]"
|
||||
|
||||
|
||||
# Coordinator routes to reviewer
|
||||
- source: coordinate
|
||||
target: review
|
||||
condition:
|
||||
type: content_contains
|
||||
text: "[ROUTE:REVIEWER]"
|
||||
|
||||
|
||||
# Coordinator routes to file manager
|
||||
- source: coordinate
|
||||
target: manage_file
|
||||
condition:
|
||||
type: content_contains
|
||||
text: "[ROUTE:FILE_MANAGER]"
|
||||
|
||||
|
||||
# Coordinator ends workflow
|
||||
- source: coordinate
|
||||
target: end
|
||||
condition:
|
||||
type: content_contains
|
||||
text: "[ROUTE:END]"
|
||||
|
||||
|
||||
# After research, go back to coordinator for next decision
|
||||
- source: research
|
||||
target: coordinate
|
||||
|
||||
|
||||
# After writing, go back to coordinator
|
||||
- source: write
|
||||
target: coordinate
|
||||
|
||||
|
||||
# After review, go back to coordinator
|
||||
- source: review
|
||||
target: coordinate
|
||||
|
||||
|
||||
# File manager routes to read or write based on JSON command
|
||||
- source: manage_file
|
||||
target: read_file
|
||||
condition:
|
||||
type: content_contains
|
||||
text: '"tool": "file_read"'
|
||||
|
||||
|
||||
- source: manage_file
|
||||
target: write_file
|
||||
condition:
|
||||
type: content_contains
|
||||
text: '"tool": "file_write"'
|
||||
|
||||
|
||||
# After file operations, go back to coordinator
|
||||
- source: read_file
|
||||
target: coordinate
|
||||
|
||||
|
||||
- source: write_file
|
||||
target: coordinate
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
SERVICE AGREEMENT
|
||||
|
||||
This Service Agreement ("Agreement") is entered into as of January 15, 2024 (the "Effective Date"), by and between:
|
||||
|
||||
TechCorp Solutions Inc., a Delaware corporation with its principal place of business at 123 Tech Street, San Francisco, CA 94105 ("Provider"),
|
||||
|
||||
and
|
||||
|
||||
Global Enterprises LLC, a California limited liability company with its principal place of business at 456 Business Ave, Los Angeles, CA 90001 ("Client").
|
||||
|
||||
RECITALS
|
||||
|
||||
WHEREAS, Provider is engaged in the business of providing cloud computing and data analytics services;
|
||||
|
||||
WHEREAS, Client desires to engage Provider to provide certain cloud infrastructure and data processing services;
|
||||
|
||||
NOW, THEREFORE, in consideration of the mutual promises and covenants contained herein, the parties agree as follows:
|
||||
|
||||
1. SERVICES
|
||||
|
||||
1.1 Provider shall provide Client with access to its cloud computing platform, including but not limited to:
|
||||
- Virtual server hosting
|
||||
- Data storage and backup services
|
||||
- Network infrastructure management
|
||||
- 24/7 technical support
|
||||
|
||||
1.2 Service Level Agreement: Provider guarantees 99.9% uptime for all hosted services.
|
||||
|
||||
2. PAYMENT TERMS
|
||||
|
||||
2.1 Client shall pay Provider a monthly fee of $5,000.00 USD for the services described herein.
|
||||
|
||||
2.2 Payment shall be due within 30 days of invoice date.
|
||||
|
||||
2.3 Late payments shall incur a penalty of 1.5% per month.
|
||||
|
||||
3. TERM AND TERMINATION
|
||||
|
||||
3.1 This Agreement shall commence on the Effective Date and continue for a period of 24 months.
|
||||
|
||||
3.2 Either party may terminate this Agreement upon 90 days written notice.
|
||||
|
||||
3.3 Provider may terminate immediately for non-payment or material breach.
|
||||
|
||||
4. CONFIDENTIALITY
|
||||
|
||||
4.1 Both parties agree to maintain the confidentiality of proprietary information shared during the term of this Agreement.
|
||||
|
||||
5. GOVERNING LAW
|
||||
|
||||
5.1 This Agreement shall be governed by the laws of the State of California.
|
||||
|
||||
6. LIMITATION OF LIABILITY
|
||||
|
||||
6.1 Provider's total liability shall not exceed the amount paid by Client in the 12 months preceding the claim.
|
||||
|
||||
IN WITNESS WHEREOF, the parties have executed this Agreement as of the date first above written.
|
||||
|
||||
TechCorp Solutions Inc.
|
||||
|
||||
By: ___________________________
|
||||
Name: John Smith
|
||||
Title: Chief Technology Officer
|
||||
|
||||
Global Enterprises LLC
|
||||
|
||||
By: ___________________________
|
||||
Name: Sarah Johnson
|
||||
Title: Chief Executive Officer
|
||||
@@ -7,18 +7,20 @@ provide async processing capabilities for the reactive architecture.
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import threading
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import rx
|
||||
from rx import operators as ops
|
||||
from rx.core import Observable
|
||||
from rx.core import Observer
|
||||
from rx.subject import Subject
|
||||
from rx.core import Observable # type: ignore[attr-defined]
|
||||
from rx.core import Observer # type: ignore[attr-defined]
|
||||
from rx.subject import Subject # type: ignore[attr-defined]
|
||||
|
||||
from cleveragents.core.exceptions import AgentCreationError
|
||||
from cleveragents.core.exceptions import ExecutionError
|
||||
@@ -65,16 +67,16 @@ class Agent(ABC):
|
||||
# Set up processing pipeline
|
||||
self._setup_processing_pipeline()
|
||||
|
||||
def _setup_processing_pipeline(self):
|
||||
def _setup_processing_pipeline(self) -> None:
|
||||
"""Set up the reactive processing pipeline."""
|
||||
self.input_stream.pipe(
|
||||
ops.map(self._process_wrapper),
|
||||
ops.flat_map(rx.from_future),
|
||||
ops.map(self._process_wrapper), # type: ignore[arg-type]
|
||||
ops.flat_map(rx.from_future), # type: ignore[arg-type]
|
||||
).subscribe(
|
||||
on_next=self.output_stream.on_next, on_error=self.output_stream.on_error
|
||||
)
|
||||
|
||||
async def _process_wrapper(self, message_data):
|
||||
async def _process_wrapper(self, message_data: tuple[str, Dict[str, Any]]) -> str:
|
||||
"""Wrapper for async processing."""
|
||||
try:
|
||||
if isinstance(message_data, tuple):
|
||||
@@ -145,11 +147,11 @@ class Agent(ABC):
|
||||
|
||||
return metadata
|
||||
|
||||
def send_message(self, message: str, context: Optional[Dict[str, Any]] = None):
|
||||
def send_message(self, message: str, context: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Send a message to the agent's input stream."""
|
||||
self.input_stream.on_next((message, context or {}))
|
||||
|
||||
def subscribe_to_output(self, observer: Observer):
|
||||
def subscribe_to_output(self, observer: Observer) -> None:
|
||||
"""Subscribe to the agent's output stream."""
|
||||
self.output_stream.subscribe(observer)
|
||||
|
||||
@@ -157,7 +159,7 @@ class Agent(ABC):
|
||||
"""Create an observable from the agent's processing."""
|
||||
return self.output_stream.pipe()
|
||||
|
||||
def dispose(self):
|
||||
def dispose(self) -> None:
|
||||
"""Clean up the agent's resources."""
|
||||
if hasattr(self.input_stream, "dispose"):
|
||||
self.input_stream.dispose()
|
||||
@@ -176,6 +178,9 @@ class AgentWithMemory(Agent):
|
||||
memory (Dict[str, Any]): The agent's memory/state.
|
||||
"""
|
||||
|
||||
# Class-level threading lock to protect asyncio.Lock initialization
|
||||
_memory_lock_init_lock = threading.Lock()
|
||||
|
||||
def __init__(
|
||||
self, name: str, config: Dict[str, Any], template_renderer: TemplateRenderer
|
||||
):
|
||||
@@ -194,17 +199,22 @@ class AgentWithMemory(Agent):
|
||||
@property
|
||||
def _memory_lock(self) -> asyncio.Lock:
|
||||
"""Lazily create and return the memory lock, ensuring an event loop exists."""
|
||||
# Fast path: check without lock first
|
||||
if self._memory_lock_instance is None:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
# No event loop in current thread, create one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self._memory_lock_instance = asyncio.Lock()
|
||||
# Thread-safe initialization using double-checked locking
|
||||
with self._memory_lock_init_lock:
|
||||
# Double-check: another thread might have initialized it
|
||||
if self._memory_lock_instance is None:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
# No event loop in current thread, create one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self._memory_lock_instance = asyncio.Lock()
|
||||
return self._memory_lock_instance
|
||||
|
||||
async def _process_wrapper(self, message_data):
|
||||
async def _process_wrapper(self, message_data: tuple[str, Dict[str, Any]]) -> str:
|
||||
"""Wrapper that manages memory access."""
|
||||
async with self._memory_lock:
|
||||
return await super()._process_wrapper(message_data)
|
||||
@@ -266,7 +276,7 @@ class StreamableAgent(Agent):
|
||||
allowing it to be used as an operator in RxPy pipelines.
|
||||
"""
|
||||
|
||||
def as_operator(self):
|
||||
def as_operator(self) -> Callable[[Observable], Observable]:
|
||||
"""
|
||||
Return an RxPy operator that processes messages through this agent.
|
||||
|
||||
@@ -275,14 +285,14 @@ class StreamableAgent(Agent):
|
||||
"""
|
||||
|
||||
def operator(source: Observable) -> Observable:
|
||||
def subscribe(observer, scheduler=None):
|
||||
def on_next(value):
|
||||
def subscribe(observer: Observer, scheduler: Any = None) -> Any:
|
||||
def on_next(value: Any) -> None:
|
||||
self.send_message(str(value))
|
||||
|
||||
def on_error(error):
|
||||
def on_error(error: Exception) -> None:
|
||||
observer.on_error(error)
|
||||
|
||||
def on_completed():
|
||||
def on_completed() -> None:
|
||||
observer.on_completed()
|
||||
|
||||
# Subscribe to agent's output
|
||||
@@ -296,11 +306,11 @@ class StreamableAgent(Agent):
|
||||
scheduler=scheduler,
|
||||
)
|
||||
|
||||
return rx.create(subscribe) # type: ignore
|
||||
return rx.create(subscribe) # type: ignore[no-untyped-call]
|
||||
|
||||
return operator
|
||||
|
||||
def map_operator(self):
|
||||
def map_operator(self) -> Any:
|
||||
"""
|
||||
Return an RxPy map operator that processes values through this agent.
|
||||
|
||||
@@ -308,10 +318,10 @@ class StreamableAgent(Agent):
|
||||
An RxPy map operator.
|
||||
"""
|
||||
|
||||
async def process_value(value):
|
||||
async def process_value(value: Any) -> Any:
|
||||
result_future: asyncio.Future[Any] = asyncio.Future()
|
||||
|
||||
def on_result(result):
|
||||
def on_result(result: Any) -> None:
|
||||
if not result_future.done():
|
||||
result_future.set_result(result)
|
||||
|
||||
@@ -321,9 +331,9 @@ class StreamableAgent(Agent):
|
||||
|
||||
return await result_future
|
||||
|
||||
return ops.map(process_value)
|
||||
return ops.map(process_value) # type: ignore[arg-type]
|
||||
|
||||
def filter_operator(self, condition_func):
|
||||
def filter_operator(self, condition_func: Callable[[Any], bool]) -> Any:
|
||||
"""
|
||||
Return an RxPy filter operator based on agent processing.
|
||||
|
||||
@@ -334,10 +344,10 @@ class StreamableAgent(Agent):
|
||||
An RxPy filter operator.
|
||||
"""
|
||||
|
||||
async def filter_with_agent(value):
|
||||
async def filter_with_agent(value: Any) -> bool:
|
||||
result_future: asyncio.Future[bool] = asyncio.Future()
|
||||
|
||||
def on_result(result):
|
||||
def on_result(result: Any) -> None:
|
||||
if not result_future.done():
|
||||
result_future.set_result(condition_func(result))
|
||||
|
||||
@@ -347,4 +357,4 @@ class StreamableAgent(Agent):
|
||||
|
||||
return await result_future
|
||||
|
||||
return ops.filter(filter_with_agent)
|
||||
return ops.filter(filter_with_agent) # type: ignore[arg-type]
|
||||
|
||||
+227
-68
@@ -8,11 +8,12 @@ capabilities within the RxPy-based reactive architecture.
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -82,6 +83,58 @@ class ToolAgent(Agent):
|
||||
else:
|
||||
raise AgentCreationError(f"Invalid tool configuration: {tool}")
|
||||
|
||||
def _extract_json_from_message(self, message: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Extract JSON from a message that might contain markdown code blocks or extra text.
|
||||
|
||||
Args:
|
||||
message: The message that might contain JSON.
|
||||
|
||||
Returns:
|
||||
Parsed JSON dict if found, None otherwise.
|
||||
|
||||
Raises:
|
||||
json.JSONDecodeError: If message appears to be JSON but is malformed.
|
||||
"""
|
||||
message_stripped = message.strip()
|
||||
|
||||
# If message looks like JSON (starts with { and ends with }), it must be valid JSON
|
||||
if message_stripped.startswith("{") and message_stripped.endswith("}"):
|
||||
# This looks like JSON, so if it fails to parse, raise an error
|
||||
parsed = json.loads(message_stripped)
|
||||
if isinstance(parsed, dict): # ✅ Add type check
|
||||
return parsed
|
||||
return None
|
||||
|
||||
# Try to extract JSON from markdown code blocks
|
||||
# Pattern: ```json ... ``` or ``` ... ```
|
||||
code_block_pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
|
||||
match = re.search(code_block_pattern, message, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
parsed = json.loads(match.group(1))
|
||||
if isinstance(parsed, dict): # ✅ Add type check
|
||||
return parsed
|
||||
return None # Not a dict
|
||||
except json.JSONDecodeError:
|
||||
# Code block had invalid JSON, raise error
|
||||
raise
|
||||
|
||||
# Try to find any JSON object in the message
|
||||
json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
|
||||
match = re.search(json_pattern, message, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
parsed = json.loads(match.group(0))
|
||||
if isinstance(parsed, dict): # ✅ Add type check
|
||||
return parsed
|
||||
return None # Not a dict
|
||||
except json.JSONDecodeError:
|
||||
# Found JSON-like content but it's invalid, ignore and fall through
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
async def process_message(
|
||||
self, message: str, context: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
@@ -99,21 +152,26 @@ class ToolAgent(Agent):
|
||||
ExecutionError: If tool execution fails.
|
||||
"""
|
||||
try:
|
||||
# Parse tool execution request
|
||||
if message.startswith("{") and message.endswith("}"):
|
||||
# JSON format tool request
|
||||
tool_request = json.loads(message)
|
||||
# Try to extract JSON tool request from message
|
||||
tool_request = self._extract_json_from_message(message)
|
||||
|
||||
if tool_request:
|
||||
tool_name = tool_request.get("tool")
|
||||
tool_args = tool_request.get("args", {})
|
||||
else:
|
||||
# Simple format: "tool_name arg1 arg2"
|
||||
parts = message.strip().split()
|
||||
if not parts:
|
||||
raise ExecutionError("Empty tool request")
|
||||
|
||||
tool_name = parts[0]
|
||||
tool_args = {"args": parts[1:]} if len(parts) > 1 else {}
|
||||
if tool_name:
|
||||
# Execute the tool
|
||||
result = await self._execute_tool(tool_name, tool_args, context)
|
||||
return str(result)
|
||||
|
||||
# Fall back to simple format: "tool_name arg1 arg2"
|
||||
message_stripped = message.strip()
|
||||
parts = message_stripped.split()
|
||||
if not parts:
|
||||
raise ExecutionError("Empty tool request")
|
||||
|
||||
tool_name = parts[0]
|
||||
tool_args = {"args": parts[1:]} if len(parts) > 1 else {}
|
||||
|
||||
# Execute the tool
|
||||
result = await self._execute_tool(tool_name, tool_args, context)
|
||||
@@ -309,94 +367,195 @@ class ToolAgent(Agent):
|
||||
except Exception as e:
|
||||
raise ExecutionError(f"File read failed: {e}") from e
|
||||
|
||||
def _clean_control_markers(self, content: str) -> str:
|
||||
"""
|
||||
Remove control markers from content before writing to files.
|
||||
|
||||
This ensures that workflow control signals like [SECTION_COMPLETE],
|
||||
[PAPER_COMPLETE], [WRITER SPEAKING], etc., don't end up in the output files.
|
||||
|
||||
Args:
|
||||
content: The raw content with potential control markers
|
||||
|
||||
Returns:
|
||||
Cleaned content without control markers
|
||||
"""
|
||||
import re
|
||||
|
||||
# List of control markers to remove
|
||||
control_markers = [
|
||||
r'\[SECTION_COMPLETE\]',
|
||||
r'\[PAPER_COMPLETE\]',
|
||||
r'\[WRITER SPEAKING\]',
|
||||
r'\[RESEARCHER SPEAKING\]',
|
||||
r'\[REVIEWER SPEAKING\]',
|
||||
r'\[REQUIREMENTS_COMPLETE\]',
|
||||
r'\[REVIEW_COMPLETE\]',
|
||||
r'\[ROUTE:\w+\]',
|
||||
r'\[FILE_READ_SUCCESS\]',
|
||||
r'\[FILE_CONTENT_START\]',
|
||||
r'\[FILE_CONTENT_END\]',
|
||||
]
|
||||
|
||||
# Remove each marker
|
||||
cleaned = content
|
||||
for marker in control_markers:
|
||||
cleaned = re.sub(marker, '', cleaned)
|
||||
|
||||
# Clean up excessive whitespace that might result from marker removal
|
||||
# But preserve intentional paragraph breaks
|
||||
cleaned = re.sub(r'\n\n\n+', '\n\n', cleaned)
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
def _validate_file_write_args(
|
||||
self, filepath: str, content: str
|
||||
) -> None:
|
||||
"""Validate file write arguments."""
|
||||
if not filepath or not content:
|
||||
logger.error("File write requires filepath and content")
|
||||
raise ExecutionError("File write tool requires file path and content")
|
||||
|
||||
def _validate_file_path_safety(
|
||||
self, filepath: str, unsafe_mode: bool
|
||||
) -> None:
|
||||
"""Validate file path for safety in safe mode."""
|
||||
if not self.safe_mode:
|
||||
return
|
||||
|
||||
# Always block directory traversal attempts
|
||||
if ".." in filepath:
|
||||
logger.error("Directory traversal blocked for %s", filepath)
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
|
||||
# Block absolute paths unless in unsafe mode
|
||||
if filepath.startswith("/") and not unsafe_mode:
|
||||
logger.error("Absolute path blocked for %s", filepath)
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
|
||||
def _prepare_append_content(
|
||||
self, filepath: str, cleaned_content: str
|
||||
) -> str:
|
||||
"""Prepare content for append mode with proper spacing for sections."""
|
||||
# Only add spacing if content looks like a document section (starts with #)
|
||||
# This prevents breaking simple append operations
|
||||
is_section = cleaned_content.lstrip().startswith('#')
|
||||
|
||||
if not is_section:
|
||||
# Simple append without spacing
|
||||
return cleaned_content
|
||||
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
existing_content = f.read()
|
||||
|
||||
# Add spacing for section-based content
|
||||
if existing_content and not existing_content.endswith('\n\n'):
|
||||
if existing_content.endswith('\n'):
|
||||
prefix = '\n' # Has one newline, add one more
|
||||
else:
|
||||
prefix = '\n\n' # No newline at all, add two
|
||||
else:
|
||||
prefix = ''
|
||||
except FileNotFoundError:
|
||||
prefix = '' # File doesn't exist yet
|
||||
|
||||
return prefix + cleaned_content
|
||||
|
||||
def _handle_insert_position(
|
||||
self, filepath: str, cleaned_content: str, position: Union[None, int, Literal["start", "end"]]
|
||||
) -> tuple[list[str], int]:
|
||||
"""Handle insert mode positioning logic."""
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
existing_lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
existing_lines = []
|
||||
|
||||
# Ensure content ends with newline
|
||||
formatted_content = (
|
||||
cleaned_content if cleaned_content.endswith('\n')
|
||||
else cleaned_content + '\n'
|
||||
)
|
||||
|
||||
# Determine insertion position
|
||||
if position is None or position == "end":
|
||||
existing_lines.append(formatted_content)
|
||||
insert_location = len(existing_lines)
|
||||
elif position == "start":
|
||||
existing_lines.insert(0, formatted_content)
|
||||
insert_location = 1
|
||||
elif isinstance(position, int):
|
||||
line_idx = max(0, min(position - 1, len(existing_lines)))
|
||||
existing_lines.insert(line_idx, formatted_content)
|
||||
insert_location = line_idx + 1
|
||||
else:
|
||||
raise ExecutionError(
|
||||
f"Invalid position '{position}'. "
|
||||
f"Use 'start', 'end', or line number."
|
||||
)
|
||||
|
||||
return existing_lines, insert_location
|
||||
|
||||
async def _file_write_tool(
|
||||
self, args: Dict[str, Any], context: Optional[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""File writing tool with support for write, append, and insert modes."""
|
||||
filepath = args.get("file", "")
|
||||
content = args.get("content", "")
|
||||
mode = args.get("mode", "w") # "w" (write/overwrite), "a" (append), "insert" (insert at position)
|
||||
position = args.get("position", None) # For insert mode: line number or "start"/"end"
|
||||
mode = args.get("mode", "w") # w=write, a=append, insert=insert at pos
|
||||
position = args.get("position", None)
|
||||
|
||||
if not filepath or not content:
|
||||
logger.error("File write requires filepath and content")
|
||||
raise ExecutionError("File write tool requires file path and content")
|
||||
# Validate inputs
|
||||
self._validate_file_write_args(filepath, content)
|
||||
|
||||
# Check unsafe mode requirement first (can be bypassed by context)
|
||||
# Clean control markers from content before writing
|
||||
cleaned_content = self._clean_control_markers(content)
|
||||
|
||||
# Check unsafe mode requirement
|
||||
unsafe_mode = context and context.get("_unsafe_mode", False)
|
||||
|
||||
if not unsafe_mode:
|
||||
logger.error("File writing requires unsafe mode")
|
||||
raise ExecutionError("File writing requires unsafe mode")
|
||||
|
||||
if self.safe_mode:
|
||||
# Always block directory traversal attempts
|
||||
if ".." in filepath:
|
||||
logger.error("Directory traversal blocked for %s", filepath)
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
# Block absolute paths unless in unsafe mode
|
||||
if filepath.startswith("/") and not unsafe_mode:
|
||||
logger.error("Absolute path blocked for %s", filepath)
|
||||
raise ExecutionError("Unsafe file path blocked in safe mode")
|
||||
# Validate file path safety
|
||||
self._validate_file_path_safety(filepath, unsafe_mode)
|
||||
|
||||
try:
|
||||
if mode == "w":
|
||||
# Standard write (overwrite)
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return f"Successfully wrote {len(content)} characters to {filepath}"
|
||||
f.write(cleaned_content)
|
||||
return f"Successfully wrote {len(cleaned_content)} characters to {filepath}"
|
||||
|
||||
elif mode == "a":
|
||||
# Append mode
|
||||
# Append mode with proper spacing
|
||||
content_to_append = self._prepare_append_content(filepath, cleaned_content)
|
||||
with open(filepath, "a", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return f"Successfully appended {len(content)} characters to {filepath}"
|
||||
f.write(content_to_append)
|
||||
return f"Successfully appended {len(cleaned_content)} characters to {filepath}"
|
||||
|
||||
elif mode == "insert":
|
||||
# Insert mode - read existing content, insert at position, write back
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
existing_lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
# If file doesn't exist, create it with the content
|
||||
existing_lines = []
|
||||
|
||||
# Determine insertion position
|
||||
if position is None or position == "end":
|
||||
# Append at end
|
||||
existing_lines.append(content if content.endswith('\n') else content + '\n')
|
||||
insert_location = len(existing_lines)
|
||||
elif position == "start":
|
||||
# Insert at beginning
|
||||
existing_lines.insert(0, content if content.endswith('\n') else content + '\n')
|
||||
insert_location = 1
|
||||
elif isinstance(position, int):
|
||||
# Insert at specific line number (1-indexed)
|
||||
line_idx = max(0, min(position - 1, len(existing_lines)))
|
||||
formatted_content = (
|
||||
content if content.endswith('\n') else content + '\n'
|
||||
)
|
||||
existing_lines.insert(line_idx, formatted_content)
|
||||
insert_location = line_idx + 1
|
||||
else:
|
||||
raise ExecutionError(
|
||||
f"Invalid position '{position}'. "
|
||||
f"Use 'start', 'end', or line number."
|
||||
)
|
||||
|
||||
# Write back
|
||||
# Insert mode at specified position
|
||||
existing_lines, insert_location = self._handle_insert_position(
|
||||
filepath, cleaned_content, position
|
||||
)
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.writelines(existing_lines)
|
||||
|
||||
return (
|
||||
f"Successfully inserted {len(content)} characters "
|
||||
f"Successfully inserted {len(cleaned_content)} characters "
|
||||
f"at line {insert_location} in {filepath}"
|
||||
)
|
||||
|
||||
else:
|
||||
raise ExecutionError(f"Invalid mode '{mode}'. Use 'w' (write), 'a' (append), or 'insert'.")
|
||||
raise ExecutionError(
|
||||
f"Invalid mode '{mode}'. Use 'w' (write), 'a' (append), or 'insert'."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("File write failed for %s: %s", filepath, e)
|
||||
raise ExecutionError(f"File write failed: {e}") from e
|
||||
|
||||
def get_capabilities(self) -> List[str]:
|
||||
"""Get the capabilities of the tool agent."""
|
||||
capabilities = ["tool-execution", "command-execution"]
|
||||
|
||||
@@ -665,37 +665,24 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
"""
|
||||
Sanitize JSON string by escaping control characters that LLMs often forget to escape.
|
||||
|
||||
This handles cases where LLMs output literal newlines, tabs, etc. in JSON strings
|
||||
instead of properly escaped sequences.
|
||||
|
||||
Args:
|
||||
json_str: Raw JSON string that may contain unescaped control characters
|
||||
|
||||
Returns:
|
||||
str: Sanitized JSON string with control characters properly escaped
|
||||
... existing docstring ...
|
||||
"""
|
||||
|
||||
def is_valid_json(s: str) -> bool:
|
||||
"""Check if string is valid JSON."""
|
||||
try:
|
||||
json.loads(s)
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
|
||||
# Try to parse as-is first
|
||||
try:
|
||||
json.loads(json_str)
|
||||
if is_valid_json(json_str):
|
||||
return json_str # Already valid, no sanitization needed
|
||||
except json.JSONDecodeError:
|
||||
pass # Need to sanitize
|
||||
|
||||
# Strategy: Find all quoted string values and escape control characters within them
|
||||
# We need to be careful to only escape content inside string values, not the JSON structure
|
||||
|
||||
# Strategy: Find all quoted string values and escape control characters
|
||||
def escape_string_content(match: Any) -> str:
|
||||
"""
|
||||
Escape control characters in a matched string value.
|
||||
|
||||
Args:
|
||||
match: Regex match object containing the string content
|
||||
|
||||
Returns:
|
||||
str: Escaped string content with surrounding quotes
|
||||
"""
|
||||
# match.group(0) is the full match including quotes
|
||||
# match.group(1) is the content inside the quotes
|
||||
"""Escape control characters in a matched string value."""
|
||||
content = match.group(1)
|
||||
|
||||
# Escape backslashes first (to avoid double-escaping)
|
||||
@@ -716,6 +703,16 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
sanitized = re.sub(pattern, escape_string_content, json_str, flags=re.DOTALL)
|
||||
|
||||
# ✅ NEW: Validate the sanitized result
|
||||
if not is_valid_json(sanitized):
|
||||
logger.warning(
|
||||
"JSON sanitization produced invalid JSON. "
|
||||
"Original length: %d, Sanitized length: %d. "
|
||||
"The sanitization logic may need improvement.",
|
||||
len(json_str),
|
||||
len(sanitized)
|
||||
)
|
||||
|
||||
return sanitized
|
||||
|
||||
def _execute_single_tool(self, tool_name: str, tool_params: Any) -> str:
|
||||
@@ -732,7 +729,7 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
try:
|
||||
# Find the appropriate agent that can execute this tool
|
||||
target_agent = None
|
||||
for _, agent in self.agents.items():
|
||||
for agent in self.agents.values():
|
||||
if hasattr(agent, 'tools') and tool_name in agent.tools:
|
||||
target_agent = agent
|
||||
break
|
||||
@@ -871,6 +868,9 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
print(f"Executing graph '{graph_name}'...")
|
||||
|
||||
# Prepare metadata with unsafe mode and context
|
||||
|
||||
if not self.config:
|
||||
raise CleverAgentsException("Configuration not loaded")
|
||||
metadata: Dict[str, Any] = {"context": self.config.global_context}
|
||||
metadata["_unsafe_mode"] = self.unsafe
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from cleveragents.agents.base import Agent
|
||||
from cleveragents.agents.tool import ToolAgent
|
||||
from cleveragents.langgraph.state import GraphState
|
||||
from cleveragents.langgraph.state import StateUpdateMode
|
||||
|
||||
@@ -149,7 +150,6 @@ class Node:
|
||||
if state.messages:
|
||||
# Check if this is a tool agent that should receive the last message
|
||||
# (regardless of role) to process commands from other agents
|
||||
from cleveragents.agents.tool import ToolAgent
|
||||
if isinstance(agent, ToolAgent):
|
||||
# Tool agents receive the last message (likely from another agent)
|
||||
agent_input = state.messages[-1].get("content", "")
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Comprehensive test script for Legal Contract Analyzer
|
||||
# Tests: legal_contract_metadata_extractor.yaml with sample_contract.txt
|
||||
|
||||
# Change to project root
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
# Check for API key
|
||||
if [ -z "$OPENAI_API_KEY" ]; then
|
||||
echo "Error: OPENAI_API_KEY environment variable not set"
|
||||
echo "Usage: export OPENAI_API_KEY='your-key-here'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Legal Contract Analyzer - Comprehensive Test"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "This test validates the multi-agent orchestrator workflow"
|
||||
echo "for comprehensive legal contract analysis."
|
||||
echo ""
|
||||
echo "Starting test..."
|
||||
echo ""
|
||||
|
||||
# Test 1: Basic functionality test
|
||||
echo "=== TEST 1: Basic Functionality ==="
|
||||
echo "Testing contract analysis workflow..."
|
||||
echo ""
|
||||
|
||||
# Run the legal contract analyzer
|
||||
python -m cleveragents run \
|
||||
-c examples/legal_contract_metadata_extractor.yaml \
|
||||
-p "sample_contract.txt" \
|
||||
--unsafe 2>&1 | tee /tmp/contract_test_output.log
|
||||
|
||||
TEST_EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
echo ""
|
||||
echo "✅ TEST 1 PASSED: System executed successfully"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ TEST 1 FAILED: System execution failed with exit code $TEST_EXIT_CODE"
|
||||
echo "Check /tmp/contract_test_output.log for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Check if agent simulation is working
|
||||
echo ""
|
||||
echo "=== TEST 2: Agent Simulation Verification ==="
|
||||
echo "Checking if orchestrator simulates specialist agents..."
|
||||
|
||||
if grep -q "FILE_LOADER AGENT" /tmp/contract_test_output.log; then
|
||||
echo "✅ FILE_LOADER agent simulation found"
|
||||
else
|
||||
echo "❌ FILE_LOADER agent simulation not found"
|
||||
fi
|
||||
|
||||
if grep -q "TEXT_PREPROCESSOR AGENT\|CONTRACT_ANALYZER AGENT\|RISK_ASSESSOR AGENT\|JSON_FORMATTER AGENT" /tmp/contract_test_output.log; then
|
||||
echo "✅ Analysis agent simulation found"
|
||||
else
|
||||
echo "❌ Analysis agent simulation not found (may continue in next interaction)"
|
||||
fi
|
||||
|
||||
# Test 3: Check tool execution
|
||||
echo ""
|
||||
echo "=== TEST 3: Tool Execution Verification ==="
|
||||
echo "Checking if file reading tool executed properly..."
|
||||
|
||||
if grep -q "TOOL_EXECUTE:file_read" /tmp/contract_test_output.log; then
|
||||
echo "✅ File reading tool execution command found"
|
||||
else
|
||||
echo "⚠️ File reading tool execution command not found"
|
||||
fi
|
||||
|
||||
# Test 4: Check for contract content
|
||||
echo ""
|
||||
echo "=== TEST 4: Contract Loading Validation ==="
|
||||
echo "Checking if contract was loaded..."
|
||||
|
||||
if grep -q "SERVICE AGREEMENT" /tmp/contract_test_output.log; then
|
||||
echo "✅ Contract content loaded successfully"
|
||||
else
|
||||
echo "❌ Contract content not found in output"
|
||||
fi
|
||||
|
||||
# Test 5: Check for parties extraction
|
||||
echo ""
|
||||
echo "=== TEST 5: Metadata Extraction Validation ==="
|
||||
echo "Checking if contract metadata was extracted..."
|
||||
|
||||
if grep -q "TechCorp Solutions" /tmp/contract_test_output.log; then
|
||||
echo "✅ Party extraction working (TechCorp Solutions found)"
|
||||
else
|
||||
echo "❌ Party extraction not working"
|
||||
fi
|
||||
|
||||
if grep -q "Global Enterprises" /tmp/contract_test_output.log; then
|
||||
echo "✅ Party extraction working (Global Enterprises found)"
|
||||
else
|
||||
echo "❌ Party extraction not working"
|
||||
fi
|
||||
|
||||
if grep -q "\$5,000" /tmp/contract_test_output.log; then
|
||||
echo "✅ Financial terms extracted (payment amount found)"
|
||||
else
|
||||
echo "⚠️ Financial terms extraction may need additional processing"
|
||||
fi
|
||||
|
||||
# Test 6: Performance test
|
||||
echo ""
|
||||
echo "=== TEST 6: Performance Check ==="
|
||||
echo "Checking if processing completed within reasonable time..."
|
||||
|
||||
if grep -q "Single-shot processing complete" /tmp/contract_test_output.log; then
|
||||
echo "✅ Processing completed successfully"
|
||||
else
|
||||
echo "❌ Processing may not have completed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Test Summary:"
|
||||
echo "=========================================="
|
||||
|
||||
# Count agent simulations
|
||||
AGENT_MODES=$(grep -c "===.*AGENT.*===>" /tmp/contract_test_output.log)
|
||||
echo "Agent simulations found: $AGENT_MODES"
|
||||
|
||||
# Check for successful processing
|
||||
if grep -q "TechCorp Solutions" /tmp/contract_test_output.log && grep -q "Global Enterprises" /tmp/contract_test_output.log; then
|
||||
echo "✅ Contract parties successfully extracted"
|
||||
fi
|
||||
|
||||
if grep -q "SERVICE AGREEMENT" /tmp/contract_test_output.log; then
|
||||
echo "✅ Contract content successfully loaded"
|
||||
fi
|
||||
|
||||
# Overall assessment
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
echo ""
|
||||
echo "🎉 TESTS PASSED! Legal Contract Analyzer is operational!"
|
||||
echo ""
|
||||
echo "The system successfully:"
|
||||
echo " ✅ Loads contract files using tool integration"
|
||||
echo " ✅ Processes through orchestrator-based workflow"
|
||||
echo " ✅ Extracts contract metadata (parties, dates, terms)"
|
||||
echo " ✅ Uses multi-agent simulation pattern"
|
||||
echo " ✅ Handles workflow orchestration properly"
|
||||
echo ""
|
||||
echo "Note: For complete analysis with all stages (preprocessing, metadata,"
|
||||
echo "risk assessment, JSON formatting), use interactive mode to continue"
|
||||
echo "the conversation through all workflow stages."
|
||||
else
|
||||
echo ""
|
||||
echo "❌ TESTS FAILED - Review the output above"
|
||||
echo "Check /tmp/contract_test_output.log for detailed logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Test Complete!"
|
||||
echo "=========================================="
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/contract_test_output.log
|
||||
|
||||
Reference in New Issue
Block a user