Spaces:
Sleeping
Sleeping
Charles Grandjean commited on
Commit Β·
ed3da7d
1
Parent(s): bee099d
rebrand agents
Browse files- {subagents β agents}/__init__.py +0 -0
- agents/chat_agent.py +160 -0
- {subagents β agents}/doc_assistant.py +0 -0
- {subagents β agents}/doc_editor.py +11 -6
- {subagents β agents}/lawyer_messenger.py +0 -0
- {subagents β agents}/lawyer_selector.py +0 -0
- {subagents β agents}/pdf_analyzer.py +0 -1
- docs/CREATE_DRAFT_DOCUMENT.md +183 -0
{subagents β agents}/__init__.py
RENAMED
|
File without changes
|
agents/chat_agent.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Flexible LangGraph agent for cyber-legal assistant
|
| 4 |
+
Agent can call tools, process results, and decide to continue or answer
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import copy
|
| 9 |
+
import logging
|
| 10 |
+
from typing import Dict, Any, List, Optional
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from langgraph.graph import StateGraph, END
|
| 13 |
+
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
from agent_states.agent_state import AgentState
|
| 17 |
+
from utils.utils_fn import PerformanceMonitor
|
| 18 |
+
from utils.tools import tools, tools_for_client, tools_for_lawyer
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class CyberLegalAgent:
|
| 22 |
+
def __init__(self, llm, tools: List[Any] = tools, tools_facade: List[Any] = tools):
|
| 23 |
+
self.tools = tools
|
| 24 |
+
self.tools_facade = tools_facade
|
| 25 |
+
self.llm = llm
|
| 26 |
+
self.performance_monitor = PerformanceMonitor()
|
| 27 |
+
self.llm_with_tools = self.llm.bind_tools(self.tools_facade)
|
| 28 |
+
self.workflow = self._build_workflow()
|
| 29 |
+
|
| 30 |
+
def _build_workflow(self) -> StateGraph:
|
| 31 |
+
workflow = StateGraph(AgentState)
|
| 32 |
+
workflow.add_node("agent", self._agent_node)
|
| 33 |
+
workflow.add_node("tools", self._tools_node)
|
| 34 |
+
workflow.set_entry_point("agent")
|
| 35 |
+
workflow.add_conditional_edges("agent", self._should_continue, {"continue": "tools", "end": END})
|
| 36 |
+
workflow.add_conditional_edges("tools", self._after_tools, {"continue": "agent", "end": END})
|
| 37 |
+
return workflow.compile()
|
| 38 |
+
|
| 39 |
+
def _after_tools(self, state: AgentState) -> str:
|
| 40 |
+
intermediate_steps = state.get("intermediate_steps", [])
|
| 41 |
+
if not intermediate_steps:
|
| 42 |
+
return "continue"
|
| 43 |
+
|
| 44 |
+
# Check if the last message is a ToolMessage from find_lawyers
|
| 45 |
+
last_message = intermediate_steps[-1]
|
| 46 |
+
if isinstance(last_message, ToolMessage):
|
| 47 |
+
if last_message.name == "_find_lawyers":
|
| 48 |
+
logger.info("π find_lawyers tool completed - ending with tool output")
|
| 49 |
+
return "end"
|
| 50 |
+
|
| 51 |
+
return "continue"
|
| 52 |
+
|
| 53 |
+
def _should_continue(self, state: AgentState) -> str:
|
| 54 |
+
intermediate_steps = state.get("intermediate_steps", [])
|
| 55 |
+
if not intermediate_steps:
|
| 56 |
+
return "continue"
|
| 57 |
+
last_message = intermediate_steps[-1]
|
| 58 |
+
|
| 59 |
+
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
| 60 |
+
print(last_message.tool_calls)
|
| 61 |
+
logger.info(f"π§ Calling tools: {[tc['name'] for tc in last_message.tool_calls]}")
|
| 62 |
+
return "continue"
|
| 63 |
+
return "end"
|
| 64 |
+
|
| 65 |
+
async def _agent_node(self, state: AgentState) -> AgentState:
|
| 66 |
+
intermediate_steps = state.get("intermediate_steps", [])
|
| 67 |
+
|
| 68 |
+
if not intermediate_steps:
|
| 69 |
+
history = state.get("conversation_history", [])
|
| 70 |
+
# Use provided system prompt if available (not None), otherwise use the default
|
| 71 |
+
system_prompt_to_use = state.get("system_prompt")
|
| 72 |
+
jurisdiction = state.get("jurisdiction", "unknown")
|
| 73 |
+
# Deepcopy to avoid modifying the original prompt string
|
| 74 |
+
system_prompt_to_use = copy.deepcopy(system_prompt_to_use)
|
| 75 |
+
system_prompt_to_use = system_prompt_to_use.format(jurisdiction=jurisdiction)
|
| 76 |
+
logger.info(f"π Formatted system prompt with jurisdiction: {jurisdiction}")
|
| 77 |
+
|
| 78 |
+
intermediate_steps.append(SystemMessage(content=system_prompt_to_use))
|
| 79 |
+
for msg in history:
|
| 80 |
+
if isinstance(msg, dict):
|
| 81 |
+
if msg.get("role") == "user":
|
| 82 |
+
intermediate_steps.append(HumanMessage(content=msg.get("content")))
|
| 83 |
+
elif msg.get("role") == "assistant":
|
| 84 |
+
intermediate_steps.append(AIMessage(content=msg.get("content")))
|
| 85 |
+
intermediate_steps.append(HumanMessage(content=state["user_query"]))
|
| 86 |
+
|
| 87 |
+
response = await self.llm_with_tools.ainvoke(intermediate_steps)
|
| 88 |
+
intermediate_steps.append(response)
|
| 89 |
+
state["intermediate_steps"] = intermediate_steps
|
| 90 |
+
return state
|
| 91 |
+
|
| 92 |
+
async def _tools_node(self, state: AgentState) -> AgentState:
|
| 93 |
+
intermediate_steps = state.get("intermediate_steps", [])
|
| 94 |
+
last_message = intermediate_steps[-1]
|
| 95 |
+
if not (hasattr(last_message, 'tool_calls') and last_message.tool_calls):
|
| 96 |
+
return state
|
| 97 |
+
for tool_call in last_message.tool_calls:
|
| 98 |
+
tool_func = next((t for t in self.tools if t.name == "_" + tool_call['name']), None)
|
| 99 |
+
if tool_func:
|
| 100 |
+
# Inject parameters from state into tool calls
|
| 101 |
+
args = tool_call['args'].copy()
|
| 102 |
+
|
| 103 |
+
# Inject conversation_history for tools that need it
|
| 104 |
+
if tool_call['name'] in ["find_lawyers", "query_knowledge_graph", "message_lawyer"]:
|
| 105 |
+
args["conversation_history"] = state.get("conversation_history", [])
|
| 106 |
+
logger.info(f"π Injecting conversation_history to {tool_call['name']}: {len(args['conversation_history'])} messages")
|
| 107 |
+
|
| 108 |
+
# Inject jurisdiction for query_knowledge_graph tool
|
| 109 |
+
if tool_call['name'] == "query_knowledge_graph":
|
| 110 |
+
args["jurisdiction"] = state.get("jurisdiction")
|
| 111 |
+
logger.info(f"π Injecting jurisdiction: {args['jurisdiction']}")
|
| 112 |
+
|
| 113 |
+
# Inject user_id for message_lawyer tool
|
| 114 |
+
if tool_call['name'] == "message_lawyer":
|
| 115 |
+
args["user_id"] = state.get("user_id")
|
| 116 |
+
logger.info(f"π€ Injecting user_id: {args['user_id']}")
|
| 117 |
+
|
| 118 |
+
# Inject user_id for retrieve_lawyer_document tool
|
| 119 |
+
if tool_call['name'] == "retrieve_lawyer_document":
|
| 120 |
+
args["user_id"] = state.get("user_id")
|
| 121 |
+
logger.info(f"π Injecting user_id for retrieve_lawyer_document: {args['user_id']}")
|
| 122 |
+
|
| 123 |
+
# Inject user_id for create_draft_document tool
|
| 124 |
+
if tool_call['name'] == "create_draft_document":
|
| 125 |
+
args["user_id"] = state.get("user_id")
|
| 126 |
+
logger.info(f"π Injecting user_id for create_draft_document: {args['user_id']}")
|
| 127 |
+
|
| 128 |
+
tool_call['name']="_" + tool_call['name']
|
| 129 |
+
|
| 130 |
+
result = await tool_func.ainvoke(args)
|
| 131 |
+
logger.info(f"π§ Tool {tool_call} returned: {result}")
|
| 132 |
+
intermediate_steps.append(ToolMessage(content=str(result), tool_call_id=tool_call['id'], name=tool_call['name']))
|
| 133 |
+
|
| 134 |
+
state["intermediate_steps"] = intermediate_steps
|
| 135 |
+
return state
|
| 136 |
+
|
| 137 |
+
async def process_query(self, user_query: str, user_id: Optional[str] = None, jurisdiction: str = "Romania", conversation_history: Optional[List[Dict[str, str]]] = None, system_prompt: Optional[str] = None) -> Dict[str, Any]:
|
| 138 |
+
initial_state = {
|
| 139 |
+
"user_query": user_query,
|
| 140 |
+
"user_id": user_id,
|
| 141 |
+
"conversation_history": conversation_history or [],
|
| 142 |
+
"intermediate_steps": [],
|
| 143 |
+
"relevant_documents": [],
|
| 144 |
+
"query_timestamp": datetime.now().isoformat(),
|
| 145 |
+
"processing_time": None,
|
| 146 |
+
"jurisdiction": jurisdiction,
|
| 147 |
+
"system_prompt": system_prompt
|
| 148 |
+
}
|
| 149 |
+
self.performance_monitor.reset()
|
| 150 |
+
|
| 151 |
+
final_state = await self.workflow.ainvoke(initial_state)
|
| 152 |
+
intermediate_steps = final_state.get("intermediate_steps", [])
|
| 153 |
+
final_response = intermediate_steps[-1].content
|
| 154 |
+
|
| 155 |
+
return {
|
| 156 |
+
"response": final_response or "I apologize, but I couldn't generate a response.",
|
| 157 |
+
"processing_time": sum(self.performance_monitor.get_metrics().values()),
|
| 158 |
+
"references": final_state.get("relevant_documents", []),
|
| 159 |
+
"timestamp": final_state.get("query_timestamp")
|
| 160 |
+
}
|
{subagents β agents}/doc_assistant.py
RENAMED
|
File without changes
|
{subagents β agents}/doc_editor.py
RENAMED
|
@@ -11,11 +11,12 @@ from langgraph.graph import StateGraph, END
|
|
| 11 |
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
|
| 12 |
|
| 13 |
from agent_states.doc_editor_state import DocEditorState
|
| 14 |
-
from utils.
|
| 15 |
-
|
|
|
|
| 16 |
)
|
| 17 |
from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
|
| 18 |
-
from utils.
|
| 19 |
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
|
@@ -23,21 +24,25 @@ logger = logging.getLogger(__name__)
|
|
| 23 |
class DocumentEditorAgent:
|
| 24 |
"""Agent for editing HTML documents using Cline-like iterative approach."""
|
| 25 |
|
| 26 |
-
def __init__(self, llm, llm_tool_calling):
|
| 27 |
"""
|
| 28 |
Initialize the document editor agent.
|
| 29 |
|
| 30 |
Args:
|
| 31 |
llm: LLM principal pour la gΓ©nΓ©ration du rΓ©sumΓ© final
|
| 32 |
llm_tool_calling: LLM pour les tool calls
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
self.llm = llm
|
| 35 |
self.llm_tool_calling = llm_tool_calling
|
| 36 |
-
self.tools =
|
| 37 |
-
self.
|
|
|
|
| 38 |
logger.info("π§ Tool binding configured with tool_choice='any' to force tool calls")
|
| 39 |
logger.info(f"π€ Using {type(llm_tool_calling).__name__} for tool calling")
|
| 40 |
logger.info(f"π Using {type(llm).__name__} for summary generation")
|
|
|
|
| 41 |
self.workflow = self._build_workflow()
|
| 42 |
|
| 43 |
def _build_workflow(self) -> StateGraph:
|
|
|
|
| 11 |
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
|
| 12 |
|
| 13 |
from agent_states.doc_editor_state import DocEditorState
|
| 14 |
+
from utils.tools import (
|
| 15 |
+
tools_for_doc_editor_facade as doc_editor_tools_facade,
|
| 16 |
+
tools_for_doc_editor as doc_editor_tools
|
| 17 |
)
|
| 18 |
from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
|
| 19 |
+
from utils.utils_fn import push_document_update
|
| 20 |
|
| 21 |
logger = logging.getLogger(__name__)
|
| 22 |
|
|
|
|
| 24 |
class DocumentEditorAgent:
|
| 25 |
"""Agent for editing HTML documents using Cline-like iterative approach."""
|
| 26 |
|
| 27 |
+
def __init__(self, llm, llm_tool_calling, tools=doc_editor_tools, tools_facade=doc_editor_tools_facade):
|
| 28 |
"""
|
| 29 |
Initialize the document editor agent.
|
| 30 |
|
| 31 |
Args:
|
| 32 |
llm: LLM principal pour la gΓ©nΓ©ration du rΓ©sumΓ© final
|
| 33 |
llm_tool_calling: LLM pour les tool calls
|
| 34 |
+
tools: Liste des tools d'implΓ©mentation (avec doc_text injectΓ©)
|
| 35 |
+
tools_facade: Liste des tools faΓ§ades (pour le LLM)
|
| 36 |
"""
|
| 37 |
self.llm = llm
|
| 38 |
self.llm_tool_calling = llm_tool_calling
|
| 39 |
+
self.tools = tools
|
| 40 |
+
self.tools_facade = tools_facade
|
| 41 |
+
self.llm_with_tools = self.llm_tool_calling.bind_tools(self.tools_facade, tool_choice="any")
|
| 42 |
logger.info("π§ Tool binding configured with tool_choice='any' to force tool calls")
|
| 43 |
logger.info(f"π€ Using {type(llm_tool_calling).__name__} for tool calling")
|
| 44 |
logger.info(f"π Using {type(llm).__name__} for summary generation")
|
| 45 |
+
logger.info(f"π οΈ Tools loaded: {len(self.tools_facade)} facade, {len(self.tools)} implementation")
|
| 46 |
self.workflow = self._build_workflow()
|
| 47 |
|
| 48 |
def _build_workflow(self) -> StateGraph:
|
{subagents β agents}/lawyer_messenger.py
RENAMED
|
File without changes
|
{subagents β agents}/lawyer_selector.py
RENAMED
|
File without changes
|
{subagents β agents}/pdf_analyzer.py
RENAMED
|
@@ -9,7 +9,6 @@ import pypdf
|
|
| 9 |
from typing import Optional
|
| 10 |
from langgraph.graph import StateGraph, END
|
| 11 |
from langchain_openai import ChatOpenAI
|
| 12 |
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 13 |
from langchain_core.messages import HumanMessage, SystemMessage
|
| 14 |
from mistralai import Mistral
|
| 15 |
|
|
|
|
| 9 |
from typing import Optional
|
| 10 |
from langgraph.graph import StateGraph, END
|
| 11 |
from langchain_openai import ChatOpenAI
|
|
|
|
| 12 |
from langchain_core.messages import HumanMessage, SystemMessage
|
| 13 |
from mistralai import Mistral
|
| 14 |
|
docs/CREATE_DRAFT_DOCUMENT.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# create_draft_document Tool
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The `create_draft_document` tool allows users (both clients and lawyers) to create and save new document drafts directly to their "My Documents" storage in Supabase.
|
| 6 |
+
|
| 7 |
+
## Tool Specifications
|
| 8 |
+
|
| 9 |
+
### Facade Version (User-facing)
|
| 10 |
+
|
| 11 |
+
```python
|
| 12 |
+
@tool
|
| 13 |
+
async def create_draft_document(
|
| 14 |
+
title: str,
|
| 15 |
+
content: str,
|
| 16 |
+
path: str
|
| 17 |
+
) -> str
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
**Parameters:**
|
| 21 |
+
- `title` (str): Document title (e.g., "Contract de bail", "Note juridique")
|
| 22 |
+
- `content` (str): Document content in HTML format (e.g., "<h1>Title</h1><p>Content...</p>")
|
| 23 |
+
- `path` (str): Folder path where to save the document
|
| 24 |
+
- Empty string `""` β root folder of My Documents
|
| 25 |
+
- `"Contracts/"` β ./Contracts/title.pdf
|
| 26 |
+
- `"Drafts/Legal/"` β ./Drafts/Legal/title.pdf
|
| 27 |
+
|
| 28 |
+
**Returns:**
|
| 29 |
+
- Confirmation message with document path and success status
|
| 30 |
+
|
| 31 |
+
### Real Implementation (With user_id injection)
|
| 32 |
+
|
| 33 |
+
```python
|
| 34 |
+
@tool
|
| 35 |
+
async def _create_draft_document(
|
| 36 |
+
user_id: str,
|
| 37 |
+
title: str,
|
| 38 |
+
content: str,
|
| 39 |
+
path: str
|
| 40 |
+
) -> str
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**Additional Parameter:**
|
| 44 |
+
- `user_id` (str): User UUID (automatically injected by the agent from state)
|
| 45 |
+
|
| 46 |
+
## Usage Examples
|
| 47 |
+
|
| 48 |
+
### Example 1: Save to root folder
|
| 49 |
+
```python
|
| 50 |
+
create_draft_document(
|
| 51 |
+
title="Mon document",
|
| 52 |
+
content="<h1>Document</h1><p>Contenu...</p>",
|
| 53 |
+
path=""
|
| 54 |
+
)
|
| 55 |
+
# Saves as: ./Mon document.pdf
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### Example 2: Save to Contracts folder
|
| 59 |
+
```python
|
| 60 |
+
create_draft_document(
|
| 61 |
+
title="Contract de bail",
|
| 62 |
+
content="<h1>Contrat de bail</h1><p>Ce contrat est conclu entre...</p>",
|
| 63 |
+
path="Contracts/"
|
| 64 |
+
)
|
| 65 |
+
# Saves as: ./Contracts/Contract de bail.pdf
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Example 3: Save to nested folder
|
| 69 |
+
```python
|
| 70 |
+
create_draft_document(
|
| 71 |
+
title="Note juridique",
|
| 72 |
+
content="<h1>Note</h1><p>Contenu juridique...</p>",
|
| 73 |
+
path="Drafts/Legal/"
|
| 74 |
+
)
|
| 75 |
+
# Saves as: ./Drafts/Legal/Note juridique.pdf
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
## Path Normalization Rules
|
| 79 |
+
|
| 80 |
+
The tool automatically normalizes the path:
|
| 81 |
+
|
| 82 |
+
1. Removes leading `./` if present
|
| 83 |
+
2. Ensures trailing `/` if path is provided
|
| 84 |
+
3. Adds `.pdf` extension automatically
|
| 85 |
+
4. Builds full path as `./{path}{title}.pdf`
|
| 86 |
+
|
| 87 |
+
| Input Path | Normalized Output |
|
| 88 |
+
|------------|-------------------|
|
| 89 |
+
| `"Contracts/"` | `"./Contracts/"` |
|
| 90 |
+
| `"Contracts"` | `"./Contracts/"` |
|
| 91 |
+
| `""` | `"./"` |
|
| 92 |
+
| `"./Contracts/"` | `"./Contracts/"` |
|
| 93 |
+
| `"Drafts/Legal/"` | `"./Drafts/Legal/"` |
|
| 94 |
+
|
| 95 |
+
## API Integration
|
| 96 |
+
|
| 97 |
+
### Supabase Endpoint
|
| 98 |
+
|
| 99 |
+
- **URL:** `{SUPABASE_BASE_URL}/create-document-from-html`
|
| 100 |
+
- **Method:** POST
|
| 101 |
+
- **Headers:**
|
| 102 |
+
- `x-api-key: <CYBERLGL_API_KEY>`
|
| 103 |
+
|
| 104 |
+
### Request Body
|
| 105 |
+
|
| 106 |
+
```json
|
| 107 |
+
{
|
| 108 |
+
"userId": "uuid-de-l-utilisateur",
|
| 109 |
+
"html": "<h1>Document Title</h1><p>Content...</p>",
|
| 110 |
+
"path": "./Contracts/Document Title.pdf"
|
| 111 |
+
}
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Response Handling
|
| 115 |
+
|
| 116 |
+
The tool handles different HTTP status codes:
|
| 117 |
+
|
| 118 |
+
- `200`: Success - Document saved
|
| 119 |
+
- `400`: Bad request - Returns error details
|
| 120 |
+
- `401`: Authentication failed - Invalid API key
|
| 121 |
+
- `403`: Access denied - No permission
|
| 122 |
+
- `500`: Server error
|
| 123 |
+
|
| 124 |
+
## Integration with Agents
|
| 125 |
+
|
| 126 |
+
### Toolsets
|
| 127 |
+
|
| 128 |
+
The tool is integrated into:
|
| 129 |
+
|
| 130 |
+
- **Client Tools:** `tools_for_client_facade` and `tools_for_client`
|
| 131 |
+
- **Lawyer Tools:** `tools_for_lawyer_facade` and `tools_for_lawyer`
|
| 132 |
+
|
| 133 |
+
### Parameter Injection
|
| 134 |
+
|
| 135 |
+
The `CyberLegalAgent` automatically injects `user_id` from the agent state when calling `_create_draft_document`:
|
| 136 |
+
|
| 137 |
+
```python
|
| 138 |
+
# In agents/chat_agent.py
|
| 139 |
+
if tool_call['name'] == "create_draft_document":
|
| 140 |
+
args["user_id"] = state.get("user_id")
|
| 141 |
+
logger.info(f"π Injecting user_id for create_draft_document: {args['user_id']}")
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
## Configuration Requirements
|
| 145 |
+
|
| 146 |
+
The following environment variables must be set:
|
| 147 |
+
|
| 148 |
+
- `SUPABASE_BASE_URL`: Base URL for Supabase functions
|
| 149 |
+
- `CYBERLGL_API_KEY`: API key for authentication
|
| 150 |
+
|
| 151 |
+
## Error Handling
|
| 152 |
+
|
| 153 |
+
The tool includes comprehensive error handling:
|
| 154 |
+
|
| 155 |
+
- Timeout errors (30s timeout)
|
| 156 |
+
- Connection errors
|
| 157 |
+
- JSON parsing errors
|
| 158 |
+
- HTTP status code errors
|
| 159 |
+
- Configuration errors (missing environment variables)
|
| 160 |
+
|
| 161 |
+
## Testing
|
| 162 |
+
|
| 163 |
+
A test file is available at `tests/test_create_draft_document.py` which tests:
|
| 164 |
+
|
| 165 |
+
1. Facade functionality
|
| 166 |
+
2. Real implementation with various path formats
|
| 167 |
+
3. Path normalization logic
|
| 168 |
+
|
| 169 |
+
## Use Cases
|
| 170 |
+
|
| 171 |
+
Users should use this tool when they want to:
|
| 172 |
+
|
| 173 |
+
- Create a new document draft
|
| 174 |
+
- Save a generated document
|
| 175 |
+
- Store a document in their document library
|
| 176 |
+
- Organize documents in specific folders
|
| 177 |
+
|
| 178 |
+
## Notes
|
| 179 |
+
|
| 180 |
+
- The `.pdf` extension is added automatically
|
| 181 |
+
- The path normalization is handled transparently
|
| 182 |
+
- The tool is available to both clients and lawyers
|
| 183 |
+
- The user_id is automatically injected and not exposed to users
|