From 3aa3d6238906b402ca6ecaa19dcb84e49077f44d Mon Sep 17 00:00:00 2001 From: Aditya Chhabra Date: Fri, 10 Oct 2025 19:38:52 +0530 Subject: [PATCH] fix: thread safety AgentWithMemory and improve ToolAgent JSON parsing and mypy issues --- README.md | 9 +- .../legal_contract_metadata_extractor.yaml | 384 ------------------ .../multi_agent_paper_writer_langgraph.yaml | 148 +++---- sample_contract.txt | 69 ++++ src/cleveragents/agents/base.py | 74 ++-- src/cleveragents/agents/tool.py | 295 ++++++++++---- src/cleveragents/core/application.py | 54 +-- src/cleveragents/langgraph/nodes.py | 2 +- tests/scripts/test_legal_contract_analyzer.sh | 167 -------- 9 files changed, 444 insertions(+), 758 deletions(-) delete mode 100644 examples/legal_contract_metadata_extractor.yaml create mode 100644 sample_contract.txt delete mode 100644 tests/scripts/test_legal_contract_analyzer.sh diff --git a/README.md b/README.md index 1ef4f594c..e37627468 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/legal_contract_metadata_extractor.yaml b/examples/legal_contract_metadata_extractor.yaml deleted file mode 100644 index 946ea0b59..000000000 --- a/examples/legal_contract_metadata_extractor.yaml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/examples/multi_agent_paper_writer_langgraph.yaml b/examples/multi_agent_paper_writer_langgraph.yaml index 78f399341..b77eb8937 100644 --- a/examples/multi_agent_paper_writer_langgraph.yaml +++ b/examples/multi_agent_paper_writer_langgraph.yaml @@ -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 diff --git a/sample_contract.txt b/sample_contract.txt new file mode 100644 index 000000000..1b4693129 --- /dev/null +++ b/sample_contract.txt @@ -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 diff --git a/src/cleveragents/agents/base.py b/src/cleveragents/agents/base.py index 08ab775fa..6e8f025dd 100644 --- a/src/cleveragents/agents/base.py +++ b/src/cleveragents/agents/base.py @@ -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] diff --git a/src/cleveragents/agents/tool.py b/src/cleveragents/agents/tool.py index 0d0e9135d..35ec4b63b 100644 --- a/src/cleveragents/agents/tool.py +++ b/src/cleveragents/agents/tool.py @@ -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"] diff --git a/src/cleveragents/core/application.py b/src/cleveragents/core/application.py index ee61cbee5..8a7ac644f 100644 --- a/src/cleveragents/core/application.py +++ b/src/cleveragents/core/application.py @@ -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 diff --git a/src/cleveragents/langgraph/nodes.py b/src/cleveragents/langgraph/nodes.py index eab837e38..186c0010f 100644 --- a/src/cleveragents/langgraph/nodes.py +++ b/src/cleveragents/langgraph/nodes.py @@ -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", "") diff --git a/tests/scripts/test_legal_contract_analyzer.sh b/tests/scripts/test_legal_contract_analyzer.sh deleted file mode 100644 index be98b563d..000000000 --- a/tests/scripts/test_legal_contract_analyzer.sh +++ /dev/null @@ -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 -