Charles Grandjean commited on
Commit
89ca562
·
1 Parent(s): 8f63119

lawyer_messenger simplification

Browse files
subagents/lawyer_messenger.py CHANGED
@@ -4,15 +4,15 @@ Lawyer Messenger Agent - Identifies lawyer from conversation and sends message
4
  """
5
 
6
  import os
7
- import json
8
- import aiohttp
9
- from typing import List, Optional, Literal
10
  from dotenv import load_dotenv
11
  from langgraph.graph import StateGraph, END
12
- from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
13
- from pydantic import BaseModel, Field
14
  from logging import getLogger
15
  from prompts.lawyer_messenger import LAWYER_MESSENGER_PROMPT
 
 
 
16
 
17
  logger = getLogger(__name__)
18
  load_dotenv()
@@ -36,145 +36,44 @@ class LawyerMessengerAgent:
36
  workflow.add_edge("send_message", END)
37
  return workflow.compile()
38
 
39
- def _format_lawyers(self, lawyers: List[dict]) -> str:
40
- return "\n\n".join([
41
- f"Lawyer ID: {l['lawyer_id']}\n- Name: {l['full_name']}\n- Specialty: {l['primary_specialty']}\n- Experience Level: {l.get('experience_level', 'N/A')}\n- Years: {l.get('experience_years', 'N/A')}\n- Description: {l.get('lawyer_description', 'N/A')[:200]}..."
42
- for l in lawyers
43
- ])
44
-
45
- async def _fetch_lawyers_from_frontend(self) -> List[dict]:
46
- """Fetch lawyers from frontend API"""
47
- base_url = os.getenv("SUPABASE_BASE_URL")
48
- if not base_url:
49
- raise Exception("SUPABASE_BASE_URL not configured in environment")
50
- API_URL = f"{base_url}/functions/v1/get-lawyer-database"
51
- API_KEY = os.getenv("CYBERLGL_API_KEY")
52
-
53
- try:
54
- async with aiohttp.ClientSession() as session:
55
- async with session.get(
56
- API_URL,
57
- headers={"X-API-Key": API_KEY}
58
- ) as response:
59
- if response.status != 200:
60
- error_text = await response.text()
61
- raise Exception(f"Frontend API error: {response.status} - {error_text}")
62
- data = await response.json()
63
- if data.get('success') and data.get('data'):
64
- return data['data']
65
- else:
66
- raise Exception(f"Frontend API returned unexpected format: {data}")
67
- except aiohttp.ClientError as e:
68
- raise Exception(f"Failed to connect to frontend API: {str(e)}")
69
- except Exception as e:
70
- raise Exception(f"Error fetching lawyers from frontend: {str(e)}")
71
-
72
  async def _fetch_lawyers(self, state: dict) -> dict:
73
  """Fetch lawyers from frontend API"""
74
- try:
75
- lawyers = await self._fetch_lawyers_from_frontend()
76
- state["lawyers"] = lawyers
77
- logger.info(f"Fetched {len(lawyers)} lawyers")
78
- return state
79
- except Exception as e:
80
- logger.error(f"Failed to fetch lawyers: {str(e)}")
81
- state["error"] = str(e)
82
- state["result"] = f"❌ Error: {str(e)}"
83
- state["message_sent"] = False
84
- # Skip to end
85
- return state
86
 
87
  async def _identify_lawyer(self, state: dict) -> dict:
88
  """Use LLM to identify lawyer and extract message from conversation"""
89
-
90
- # Check if we have lawyers
91
  lawyers = state.get("lawyers", [])
92
- if not lawyers or state.get("error"):
93
- state["lawyer_selection"] = None
94
- state["result"] = "No lawyers available or error fetching lawyers."
95
- state["message_sent"] = False
96
- return state
97
-
98
- # Extract valid lawyer IDs
99
  valid_lawyer_ids = [l['lawyer_id'] for l in lawyers]
100
- logger.info(f"Available lawyer IDs: {valid_lawyer_ids}")
101
-
102
- # Create lawyer map for lookup
103
  lawyer_map = {l['lawyer_id']: l for l in lawyers}
104
 
105
- # Dynamically create Pydantic model with Literal types for ID validation
106
- LawyerID = Literal[tuple(valid_lawyer_ids)] if len(valid_lawyer_ids) > 0 else str
107
-
108
- class LawyerMessage(BaseModel):
109
- lawyer_id: LawyerID = Field(description=f"The unique ID of the lawyer from the retrieved list")
110
- subject: str = Field(description="Subject line for the message")
111
- message: str = Field(description="Message content to send to the lawyer")
112
 
113
- # Format lawyers for prompt
114
- lawyers_text = self._format_lawyers(lawyers)
115
- prompt = LAWYER_MESSENGER_PROMPT.format(lawyers=lawyers_text)
116
 
117
- # Convert message dicts to Message objects
118
- messages = []
119
- for msg in state["conversation_history"]:
120
- role = msg.get("role") if isinstance(msg, dict) else msg.role
121
- content = msg.get("content") if isinstance(msg, dict) else msg.content
122
-
123
- if role == "system":
124
- messages.append(SystemMessage(content=content))
125
- elif role == "user":
126
- messages.append(HumanMessage(content=content))
127
- elif role == "assistant":
128
- messages.append(AIMessage(content=content))
129
 
130
- # Append messenger prompt
131
- messages.append(HumanMessage(content=prompt))
132
-
133
- # Use dynamic structured output
134
- try:
135
- result = await self.llm.with_structured_output(LawyerMessage).ainvoke(messages)
136
- logger.info(f"Identified lawyer: {result.lawyer_id}")
137
- logger.info(f"Subject: {result.subject}")
138
- logger.info(f"Message length: {len(result.message)}")
139
-
140
- state["lawyer_selection"] = {
141
- "lawyer_id": result.lawyer_id,
142
- "subject": result.subject,
143
- "message": result.message,
144
- "lawyer": lawyer_map.get(result.lawyer_id)
145
- }
146
- return state
147
- except Exception as e:
148
- logger.error(f"Structured output error: {str(e)}")
149
- state["lawyer_selection"] = None
150
- state["error"] = str(e)
151
- state["result"] = f"❌ Error identifying lawyer: {str(e)}"
152
- state["message_sent"] = False
153
- return state
154
 
155
  async def _send_message(self, state: dict) -> dict:
156
  """Send message to lawyer via frontend API"""
157
-
158
- # Check if we have a lawyer selection
159
  lawyer_selection = state.get("lawyer_selection")
160
-
161
- if not lawyer_selection or not lawyer_selection.get("lawyer_id"):
162
- state["result"] = "❌ No lawyer identified. The client hasn't clearly specified which lawyer to contact."
163
- state["message_sent"] = False
164
- return state
165
-
166
- # Check for empty message (LLM decided not to send)
167
- if not lawyer_selection.get("message") or lawyer_selection.get("message").strip() == "":
168
- state["result"] = "❌ No message to send. The client hasn't provided a clear message."
169
- state["message_sent"] = False
170
- return state
171
-
172
- # Prepare API request
173
  base_url = os.getenv("SUPABASE_BASE_URL")
174
- if not base_url:
175
- raise Exception("SUPABASE_BASE_URL not configured in environment")
176
- API_URL = f"{base_url}/functions/v1/send-message-agent"
177
- API_KEY = os.getenv("CYBERLGL_API_KEY")
178
 
179
  payload = {
180
  "userId": state["user_id"],
@@ -183,49 +82,24 @@ class LawyerMessengerAgent:
183
  "message": lawyer_selection["message"]
184
  }
185
 
186
- try:
187
- async with aiohttp.ClientSession() as session:
188
- async with session.post(
189
- API_URL,
190
- headers={
191
- "x-api-key": API_KEY,
192
- "Content-Type": "application/json"
193
- },
194
- json=payload
195
- ) as response:
196
- if response.status != 200:
197
- error_text = await response.text()
198
- raise Exception(f"Send message API error: {response.status} - {error_text}")
199
-
200
- result_data = await response.json()
201
- logger.info(f"Message sent successfully: {result_data}")
202
-
203
- # Format success response
204
- lawyer = lawyer_selection.get("lawyer", {})
205
- lawyer_name = lawyer.get("full_name", "Unknown")
206
-
207
- state["result"] = (
208
- f"✅ Message sent successfully!\n\n"
209
- f"📨 To: {lawyer_name}\n"
210
- f"📌 Lawyer ID: {lawyer_selection['lawyer_id']}\n"
211
- f"📝 Subject: {lawyer_selection['subject']}\n\n"
212
- f"The lawyer will receive your message and respond to you shortly."
213
- )
214
- state["message_sent"] = True
215
- return state
216
-
217
- except aiohttp.ClientError as e:
218
- logger.error(f"Failed to send message: {str(e)}")
219
- state["result"] = f"❌ Failed to send message: {str(e)}"
220
- state["error"] = str(e)
221
- state["message_sent"] = False
222
- return state
223
- except Exception as e:
224
- logger.error(f"Error sending message: {str(e)}")
225
- state["result"] = f"❌ Error sending message: {str(e)}"
226
- state["error"] = str(e)
227
- state["message_sent"] = False
228
- return state
229
 
230
  async def send_lawyer_message(self, conversation_history: List[dict], user_id: str) -> str:
231
  """Main entry point: identify lawyer and send message"""
@@ -238,4 +112,4 @@ class LawyerMessengerAgent:
238
  "result": "",
239
  "error": None
240
  })
241
- return result["result"]
 
4
  """
5
 
6
  import os
7
+ from typing import List
 
 
8
  from dotenv import load_dotenv
9
  from langgraph.graph import StateGraph, END
10
+ from langchain_core.messages import HumanMessage
 
11
  from logging import getLogger
12
  from prompts.lawyer_messenger import LAWYER_MESSENGER_PROMPT
13
+ from utils.lawyer_messenger_utils import (
14
+ call_api, convert_messages, format_lawyers_for_prompt, create_lawyer_message_model
15
+ )
16
 
17
  logger = getLogger(__name__)
18
  load_dotenv()
 
36
  workflow.add_edge("send_message", END)
37
  return workflow.compile()
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  async def _fetch_lawyers(self, state: dict) -> dict:
40
  """Fetch lawyers from frontend API"""
41
+ base_url = os.getenv("SUPABASE_BASE_URL")
42
+ data = await call_api(
43
+ "GET",
44
+ f"{base_url}/functions/v1/get-lawyer-database",
45
+ os.getenv("CYBERLGL_API_KEY")
46
+ )
47
+
48
+ state["lawyers"] = data['data']
49
+ return state
 
 
 
50
 
51
  async def _identify_lawyer(self, state: dict) -> dict:
52
  """Use LLM to identify lawyer and extract message from conversation"""
 
 
53
  lawyers = state.get("lawyers", [])
 
 
 
 
 
 
 
54
  valid_lawyer_ids = [l['lawyer_id'] for l in lawyers]
 
 
 
55
  lawyer_map = {l['lawyer_id']: l for l in lawyers}
56
 
57
+ LawyerMessage = create_lawyer_message_model(valid_lawyer_ids)
58
+ lawyers_text = format_lawyers_for_prompt(lawyers)
 
 
 
 
 
59
 
60
+ messages = convert_messages(state["conversation_history"])
61
+ messages.append(HumanMessage(content=LAWYER_MESSENGER_PROMPT.format(lawyers=lawyers_text)))
 
62
 
63
+ result = await self.llm.with_structured_output(LawyerMessage).ainvoke(messages)
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ state["lawyer_selection"] = {
66
+ "lawyer_id": result.lawyer_id,
67
+ "subject": result.subject,
68
+ "message": result.message,
69
+ "lawyer": lawyer_map.get(result.lawyer_id)
70
+ }
71
+ return state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  async def _send_message(self, state: dict) -> dict:
74
  """Send message to lawyer via frontend API"""
 
 
75
  lawyer_selection = state.get("lawyer_selection")
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  base_url = os.getenv("SUPABASE_BASE_URL")
 
 
 
 
77
 
78
  payload = {
79
  "userId": state["user_id"],
 
82
  "message": lawyer_selection["message"]
83
  }
84
 
85
+ await call_api(
86
+ "POST",
87
+ f"{base_url}/functions/v1/send-message-agent",
88
+ os.getenv("CYBERLGL_API_KEY"),
89
+ payload
90
+ )
91
+
92
+ lawyer_name = lawyer_selection.get("lawyer", {}).get("full_name", "Unknown")
93
+ state["result"] = (
94
+ f"✅ Message sent successfully!\n\n"
95
+ f"📨 To: {lawyer_name}\n"
96
+ f"📌 Lawyer ID: {lawyer_selection['lawyer_id']}\n"
97
+ f"📝 Subject: {lawyer_selection['subject']}\n\n"
98
+ f"The lawyer will receive your message and respond shortly."
99
+ )
100
+ state["message_sent"] = True
101
+
102
+ return state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  async def send_lawyer_message(self, conversation_history: List[dict], user_id: str) -> str:
105
  """Main entry point: identify lawyer and send message"""
 
112
  "result": "",
113
  "error": None
114
  })
115
+ return result["result"]
utils/lawyer_messenger_utils.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utility functions for Lawyer Messenger Agent
4
+ """
5
+
6
+ import os
7
+ import aiohttp
8
+ from typing import List, Optional, Literal
9
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ async def call_api(
14
+ method: str,
15
+ url: str,
16
+ api_key: str,
17
+ payload: Optional[dict] = None,
18
+ headers: Optional[dict] = None
19
+ ) -> dict:
20
+ """
21
+ Generic API call handler for GET and POST requests
22
+
23
+ Args:
24
+ method: HTTP method ('GET' or 'POST')
25
+ url: API endpoint URL
26
+ api_key: API key for authentication
27
+ payload: Optional JSON payload for POST requests
28
+ headers: Optional additional headers
29
+
30
+ Returns:
31
+ Response data as dict
32
+
33
+ Raises:
34
+ Exception: If API call fails or returns non-200 status
35
+ """
36
+ default_headers = {
37
+ "X-API-Key": api_key,
38
+ "Content-Type": "application/json"
39
+ }
40
+
41
+ if headers:
42
+ default_headers.update(headers)
43
+
44
+ try:
45
+ async with aiohttp.ClientSession() as session:
46
+ if method.upper() == "GET":
47
+ async with session.get(url, headers=default_headers) as response:
48
+ if response.status != 200:
49
+ error_text = await response.text()
50
+ raise Exception(f"API error: {response.status} - {error_text}")
51
+ return await response.json()
52
+
53
+ elif method.upper() == "POST":
54
+ async with session.post(url, headers=default_headers, json=payload) as response:
55
+ if response.status != 200:
56
+ error_text = await response.text()
57
+ raise Exception(f"API error: {response.status} - {error_text}")
58
+ return await response.json()
59
+
60
+ except aiohttp.ClientError as e:
61
+ raise Exception(f"Connection failed: {str(e)}")
62
+
63
+
64
+ def convert_messages(conversation_history: List[dict]) -> list:
65
+ """
66
+ Convert conversation history dict to LangChain Message objects
67
+
68
+ Args:
69
+ conversation_history: List of message dicts with 'role' and 'content'
70
+
71
+ Returns:
72
+ List of LangChain Message objects
73
+ """
74
+ messages = []
75
+ for msg in conversation_history:
76
+ role = msg.get("role")
77
+ content = msg.get("content")
78
+
79
+ if role == "system":
80
+ messages.append(SystemMessage(content=content))
81
+ elif role == "user":
82
+ messages.append(HumanMessage(content=content))
83
+ elif role == "assistant":
84
+ messages.append(AIMessage(content=content))
85
+
86
+ return messages
87
+
88
+
89
+ def format_lawyers_for_prompt(lawyers: List[dict]) -> str:
90
+ """
91
+ Format lawyer list for LLM prompt
92
+
93
+ Args:
94
+ lawyers: List of lawyer dicts
95
+
96
+ Returns:
97
+ Formatted string with lawyer information
98
+ """
99
+ return "\n\n".join([
100
+ f"Lawyer ID: {l['lawyer_id']}\n"
101
+ f"- Name: {l['full_name']}\n"
102
+ f"- Specialty: {l['primary_specialty']}\n"
103
+ f"- Experience: {l.get('experience_level', 'N/A')} ({l.get('experience_years', 'N/A')} years)\n"
104
+ f"- Description: {l.get('lawyer_description', 'N/A')[:200]}..."
105
+ for l in lawyers
106
+ ])
107
+
108
+
109
+ def create_lawyer_message_model(valid_lawyer_ids: List[str]) -> BaseModel:
110
+ """
111
+ Dynamically create Pydantic model for lawyer message selection
112
+
113
+ Args:
114
+ valid_lawyer_ids: List of valid lawyer IDs
115
+
116
+ Returns:
117
+ Pydantic BaseModel with Literal types for ID validation
118
+ """
119
+ LawyerID = Literal[tuple(valid_lawyer_ids)] if len(valid_lawyer_ids) > 0 else str
120
+
121
+ class LawyerMessage(BaseModel):
122
+ lawyer_id: LawyerID = Field(description="Unique ID of the selected lawyer")
123
+ subject: str = Field(description="Subject line for the message")
124
+ message: str = Field(description="Message content to send to the lawyer")
125
+
126
+ return LawyerMessage