muhammadbinmurtza
Restructure: clauseguard as package subfolder, app_file: clauseguard/app.py
913a064
"""ClauseGuard Copilot Agent — interactive AI chat assistant for contract analysis.
This agent handles multi-turn conversations where users ask questions about
their analyzed contract. It uses the full contract text and the completed
clause analysis report as context, and responds via the Qwen model.
"""
import asyncio
import logging
from typing import Any, Dict, List
from clauseguard.config.copilot_prompts import COPILOT_SYSTEM_PROMPT
from clauseguard.models.report import FinalReport
from clauseguard.services.model_service import call_model_chat
logger = logging.getLogger(__name__)
CHAT_TIMEOUT_SECONDS = 60
def build_contract_context(full_contract_text: str, report: FinalReport) -> str:
"""Build a detailed context string from the contract and its analysis.
This context is injected into every copilot conversation so the AI can
reference specific clauses, severity levels, and recommended fixes.
Args:
full_contract_text: The raw text of the original contract.
report: The completed FinalReport from the analysis pipeline.
Returns:
A formatted context string for the copilot.
"""
parts: List[str] = []
parts.append("=" * 60)
parts.append("FULL CONTRACT TEXT")
parts.append("=" * 60)
parts.append(full_contract_text)
parts.append("")
parts.append("=" * 60)
parts.append("CLAUSE-BY-CLAUSE ANALYSIS")
parts.append("=" * 60)
parts.append(
f"Contract Type: {report.summary.contract_type} | "
f"Total Clauses: {report.summary.total_clauses} | "
f"Risk Score: {report.summary.overall_score}/10"
)
parts.append(
f"Critical: {report.summary.critical_count} | "
f"High: {report.summary.high_count} | "
f"Medium: {report.summary.medium_count} | "
f"Low: {report.summary.low_count}"
)
parts.append("")
for i, sc in enumerate(report.scored_clauses, 1):
parts.append(f"--- Clause {i} ---")
parts.append(f"Original Text: {sc.clause.raw_text}")
parts.append(f"Clause Type: {sc.clause.clause_type.value}")
if sc.clause.section_heading:
parts.append(f"Section: {sc.clause.section_heading}")
parts.append(f"Severity: {sc.finding.severity.value}")
parts.append(f"Risk Title: {sc.finding.risk_title}")
parts.append(f"Risk Reason: {sc.finding.risk_reason}")
if sc.clause.plain_english:
parts.append(f"Plain English: {sc.clause.plain_english}")
if sc.finding.recommended_action:
parts.append(f"Recommended Action: {sc.finding.recommended_action}")
if sc.finding.safer_clause_version:
parts.append(f"Safer Wording: {sc.finding.safer_clause_version}")
if sc.finding.negotiation_message:
parts.append(f"Negotiation Message: {sc.finding.negotiation_message}")
if sc.finding.impact_scenarios:
parts.append("Impact Scenarios:")
for impact in sc.finding.impact_scenarios:
parts.append(f" - {impact}")
parts.append("")
if report.top_3_actions:
parts.append("=" * 60)
parts.append("TOP 3 RECOMMENDED ACTIONS")
parts.append("=" * 60)
for j, action in enumerate(report.top_3_actions, 1):
parts.append(f"{j}. {action}")
parts.append("")
return "\n".join(parts)
def build_chat_messages(
system_prompt: str,
contract_context: str,
chat_history: List[Dict[str, str]],
user_message: str,
) -> List[Dict[str, str]]:
"""Build the full message list for the copilot chat API call.
Args:
system_prompt: The copilot system prompt.
contract_context: The formatted contract + analysis context.
chat_history: Previous messages in the conversation.
user_message: The new user message to respond to.
Returns:
A list of message dicts ready for the OpenAI chat API.
"""
full_system = f"{system_prompt}\n\n---\n\n## CONTRACT CONTEXT\n\n{contract_context}"
messages: List[Dict[str, str]] = [
{"role": "system", "content": full_system},
]
for msg in chat_history:
messages.append({"role": msg["role"], "content": msg["content"]})
messages.append({"role": "user", "content": user_message})
return messages
async def run_copilot(
contract_context: str,
chat_history: List[Dict[str, str]],
user_message: str,
) -> str:
"""Send a user message to the copilot and return the assistant's response.
Args:
contract_context: The formatted contract + analysis context string.
chat_history: Previous messages in the conversation (role/content dicts).
user_message: The new question from the user.
Returns:
The assistant's text response, or an error message on failure.
"""
messages = build_chat_messages(COPILOT_SYSTEM_PROMPT, contract_context, chat_history, user_message)
return await call_model_chat(messages, timeout=CHAT_TIMEOUT_SECONDS)
# ── Python 3.10+ compat: same function available as synchronous wrapper for Streamlit ──
def run_copilot_sync(
contract_context: str,
chat_history: List[Dict[str, str]],
user_message: str,
) -> str:
"""Synchronous wrapper around run_copilot for use in Streamlit callbacks.
Streamlit's chat input callback runs in the main thread, so we launch
a fresh event loop to run the async copilot call.
"""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
run_copilot(contract_context, chat_history, user_message)
)
finally:
loop.close()
return result
except Exception as e:
logger.error("run_copilot_sync failed: %s", e)
return f"Sorry, an unexpected error occurred: {e}"