Charles Grandjean commited on
Commit
ed3da7d
Β·
1 Parent(s): bee099d

rebrand agents

Browse files
{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.doc_editor_tools import (
15
- replace_html, add_html, delete_html, view_current_document, attempt_completion
 
16
  )
17
  from prompts.doc_editor import get_doc_editor_system_prompt, get_summary_system_prompt
18
- from utils.update_notifier import push_document_update
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 = [replace_html, add_html, delete_html, view_current_document, attempt_completion]
37
- self.llm_with_tools = self.llm_tool_calling.bind_tools(self.tools, tool_choice="any")
 
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