vichudo commited on
Commit
0560e17
·
1 Parent(s): 3ac6e08
Files changed (4) hide show
  1. app.py +139 -156
  2. src/agents/agent_director.py +152 -184
  3. src/models/retriever.py +19 -21
  4. temp_agent_director.py +508 -0
app.py CHANGED
@@ -172,182 +172,165 @@ def create_fallback_ui():
172
  director = None
173
  CHAT_MODEL = None
174
  try:
175
- if data_ready:
176
- from src.agents.agent_director import AgentDirector
177
- from src.utils.config import CHAT_MODEL
178
- else:
179
- raise ImportError("Data files not available - skipping imports")
180
  except ImportError as e:
181
  print(f"Error importing modules: {e}")
182
  print("This may be caused by missing data files or module dependencies.")
183
  data_ready = False
184
  traceback.print_exc()
185
 
 
 
 
 
 
 
 
 
 
186
  # Create the Gradio interface with full functionality
187
  def create_interface():
188
- with gr.Blocks(theme=gr.themes.Soft(), title="Agentic Defensor") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  with gr.Row():
190
- gr.Markdown("# 📋 Agentic Defensor")
191
- status_indicator = gr.Markdown("Status: Ready", elem_id="status-indicator")
 
 
 
 
 
 
 
192
 
193
- gr.Markdown("An agentic RAG system for legal defense analysis.")
 
 
194
 
195
- with gr.Tabs():
196
- with gr.TabItem("📝 Query"):
197
- with gr.Row():
198
- with gr.Column(scale=3):
199
- query_input = gr.Textbox(
200
- label="Legal Query",
201
- placeholder="Enter your legal query here...",
202
- lines=5
203
- )
204
-
205
- with gr.Column(scale=1):
206
- with gr.Box():
207
- gr.Markdown("### ⚙️ Settings")
208
- top_k = gr.Slider(
209
- minimum=5,
210
- maximum=50,
211
- value=10,
212
- step=5,
213
- label="Results to Consider"
214
- )
215
- debug = gr.Checkbox(label="Show Agent Reasoning", value=True)
216
-
217
- with gr.Row():
218
- submit_btn = gr.Button("Submit Query", variant="primary")
219
- clear_btn = gr.Button("Clear", variant="secondary")
220
-
221
- # Progress indicators
222
- with gr.Row(visible=False) as progress_row:
223
- progress = gr.Progress(label="Processing Query", show_progress=True)
224
- current_step = gr.Markdown("Starting...")
225
-
226
- output = gr.Markdown(label="Response")
227
-
228
- def process_query_with_status(query, top_k=50, debug=False, progress=gr.Progress()):
229
- """Process a query using the agent director with status updates."""
230
- global director
231
-
232
- if not query.strip():
233
- return "Please enter a query."
234
-
235
- # Show progress
236
- progress(0, desc="Initializing...")
237
-
238
- # Initialize the agent if not already done
239
- if director is None:
240
- try:
241
- progress(10, desc="Setting up the agent...")
242
- # Initialize the agent director
243
- director = AgentDirector(model=CHAT_MODEL, top_k=top_k)
244
- progress(20, desc="Agent ready")
245
- except Exception as e:
246
- error_details = traceback.format_exc()
247
- print(f"Agent initialization error: {e}")
248
- print(error_details)
249
- progress(100, desc="Error")
250
- return f"ERROR: Failed to initialize agent: {str(e)}\n\nDetails:\n{error_details}"
251
-
252
- # Process the query
253
- try:
254
- progress(30, desc="Analyzing your query...")
255
- time.sleep(1) # Give time for UI to update
256
-
257
- progress(40, desc="Retrieving relevant documents...")
258
- time.sleep(1) # Give time for UI to update
259
-
260
- progress(60, desc="Organizing information...")
261
- time.sleep(1) # Give time for UI to update
262
-
263
- progress(80, desc="Formulating response...")
264
- result = director.process_query(query)
265
-
266
- # Format the response
267
- answer = result.get("answer", "No answer available.")
268
-
269
- # Add sources if available
270
- if "sources" in result and result["sources"]:
271
- sources_section = "\n\n### Sources\n"
272
- for source in result["sources"]:
273
- sources_section += f"- {source}\n"
274
- answer += sources_section
275
-
276
- # Add debugging information if available
277
- if debug and result.get("reasoning_steps") is not None:
278
- reasoning = "\n\n## Agent Reasoning\n"
279
- for step in result["reasoning_steps"]:
280
- reasoning += f"\n### {step['stage']}\n{step['reasoning']}\n"
281
- answer += reasoning
282
-
283
- progress(100, desc="Completed!")
284
- return answer
285
-
286
- except Exception as e:
287
- error_details = traceback.format_exc()
288
- print(f"Query processing error: {e}")
289
- print(error_details)
290
- progress(100, desc="Error")
291
- return f"ERROR: {str(e)}\n\nDetails:\n{error_details}"
292
-
293
- def clear_inputs():
294
- return "", gr.update(visible=False), "Ready to process your query"
295
-
296
- def show_progress():
297
- return gr.update(visible=True), "Processing your query..."
298
-
299
- # Set up event handlers
300
- submit_btn.click(
301
- fn=show_progress,
302
- outputs=[progress_row, status_indicator],
303
- queue=False
304
- ).then(
305
- fn=process_query_with_status,
306
- inputs=[query_input, top_k, debug],
307
- outputs=[output],
308
- api_name="query"
309
- ).then(
310
- fn=lambda: gr.update(visible=False),
311
- outputs=[progress_row],
312
- queue=False
313
- ).then(
314
- fn=lambda: "Ready for next query",
315
- outputs=[status_indicator],
316
- queue=False
317
- )
318
-
319
- clear_btn.click(
320
- fn=clear_inputs,
321
- outputs=[query_input, progress_row, status_indicator],
322
- queue=False
323
- )
324
 
325
- with gr.TabItem("ℹ️ About"):
326
- gr.Markdown("""
327
- ## About Agentic Defensor
328
-
329
- Agentic Defensor is a multi-agent legal analysis system that uses a specialized set of agents to:
330
 
331
- 1. Analyze user queries to understand intent and extract key entities
332
- 2. Retrieve relevant document chunks from a legal knowledge base
333
- 3. Organize and aggregate context for comprehensive analysis
334
- 4. Generate detailed legal defense analyses with source references
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- This system goes beyond traditional RAG (Retrieval-Augmented Generation) by employing multiple specialized agents working together to produce high-quality legal analyses.
 
337
 
338
- ### Debug Mode
 
339
 
340
- The "Show Agent Reasoning" option allows you to see the agent's thought process during analysis, including query understanding, document retrieval decisions, context organization, and answer formulation.
 
341
 
342
- ### About This Demo
 
343
 
344
- This demo is running with fallback test data, as the full dataset is not available in this environment. In a production deployment, the system would have access to a comprehensive legal knowledge base with thousands of documents.
 
 
 
 
 
345
 
346
- ### Source Code
 
 
 
 
 
347
 
348
- [GitHub Repository](https://github.com/vichudo/agentic-defensor)
349
- """)
350
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  # Add custom CSS
352
  demo.load(
353
  js="""
 
172
  director = None
173
  CHAT_MODEL = None
174
  try:
175
+ # Use a workaround to avoid circular imports - don't import here
176
+ # We'll import these in the specific functions where needed
177
+ data_ready = data_ready and os.path.exists('src/agents/agent_director.py')
 
 
178
  except ImportError as e:
179
  print(f"Error importing modules: {e}")
180
  print("This may be caused by missing data files or module dependencies.")
181
  data_ready = False
182
  traceback.print_exc()
183
 
184
+ # Default CSS for the interface
185
+ CSS = """
186
+ .response-container {
187
+ max-height: 500px;
188
+ overflow-y: auto;
189
+ padding: 10px;
190
+ }
191
+ """
192
+
193
  # Create the Gradio interface with full functionality
194
  def create_interface():
195
+ """Create the Gradio interface."""
196
+
197
+ with gr.Blocks(
198
+ title="Defensor Legal Assistant",
199
+ theme=gr.themes.Soft(),
200
+ css=CSS
201
+ ) as demo:
202
+ gr.Markdown("# Defensor Legal Assistant")
203
+ gr.Markdown("Ask questions about legal defense strategies and get comprehensive answers based on legitimate sources.")
204
+
205
+ # Add status indicator
206
+ status_indicator = gr.Markdown("Status: Ready", elem_id="status-indicator")
207
+
208
+ query_input = gr.Textbox(
209
+ label="Your Question",
210
+ placeholder="Enter your legal question here...",
211
+ lines=2
212
+ )
213
+
214
+ # Progress indicator with no parameters
215
+ progress = gr.Progress()
216
+
217
  with gr.Row():
218
+ gr.Markdown("### ⚙️ Settings")
219
+ top_k = gr.Slider(
220
+ minimum=5,
221
+ maximum=50,
222
+ value=10,
223
+ step=5,
224
+ label="Results to Consider"
225
+ )
226
+ debug = gr.Checkbox(label="Show Agent Reasoning", value=True)
227
 
228
+ with gr.Row():
229
+ submit_btn = gr.Button("Submit Query", variant="primary")
230
+ clear_btn = gr.Button("Clear", variant="secondary")
231
 
232
+ output = gr.Markdown(label="Response")
233
+
234
+ def process_query_with_status(query, top_k=50, debug=False, progress=gr.Progress()):
235
+ """Process a query using the agent director with status updates."""
236
+ global director
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
+ if not query.strip():
239
+ return "Please enter a query."
 
 
 
240
 
241
+ # Show progress
242
+ progress(0, desc="Initializing...")
243
+
244
+ # Import here to avoid circular references
245
+ try:
246
+ from src.agents.agent_director import AgentDirector
247
+ from src.utils.config import CHAT_MODEL
248
+ except ImportError as e:
249
+ return f"ERROR: Failed to import required modules: {str(e)}"
250
+
251
+ # Initialize the agent if not already done
252
+ if director is None:
253
+ try:
254
+ progress(10, desc="Setting up the agent...")
255
+ # Initialize the agent director
256
+ director = AgentDirector(model=CHAT_MODEL, top_k=top_k)
257
+ progress(20, desc="Agent ready")
258
+ except Exception as e:
259
+ error_details = traceback.format_exc()
260
+ print(f"Agent initialization error: {e}")
261
+ print(error_details)
262
+ progress(100, desc="Error")
263
+ return f"ERROR: Failed to initialize agent: {str(e)}\n\nDetails:\n{error_details}"
264
+
265
+ # Process the query
266
+ try:
267
+ progress(30, desc="Analyzing your query...")
268
+ time.sleep(1) # Give time for UI to update
269
 
270
+ progress(40, desc="Retrieving relevant documents...")
271
+ time.sleep(1) # Give time for UI to update
272
 
273
+ progress(60, desc="Organizing information...")
274
+ time.sleep(1) # Give time for UI to update
275
 
276
+ progress(80, desc="Formulating response...")
277
+ result = director.process_query(query)
278
 
279
+ # Format the response
280
+ answer = result.get("answer", "No answer available.")
281
 
282
+ # Add sources if available
283
+ if "sources" in result and result["sources"]:
284
+ sources_section = "\n\n### Sources\n"
285
+ for source in result["sources"]:
286
+ sources_section += f"- {source}\n"
287
+ answer += sources_section
288
 
289
+ # Add debugging information if available
290
+ if debug and result.get("reasoning_steps") is not None:
291
+ reasoning = "\n\n## Agent Reasoning\n"
292
+ for step in result["reasoning_steps"]:
293
+ reasoning += f"\n### {step['stage']}\n{step['reasoning']}\n"
294
+ answer += reasoning
295
 
296
+ progress(100, desc="Completed!")
297
+ return answer
298
+
299
+ except Exception as e:
300
+ error_details = traceback.format_exc()
301
+ print(f"Query processing error: {e}")
302
+ print(error_details)
303
+ progress(100, desc="Error")
304
+ return f"ERROR: {str(e)}\n\nDetails:\n{error_details}"
305
+
306
+ def clear_inputs():
307
+ return "", "Ready to process your query"
308
+
309
+ def show_progress():
310
+ return "Processing your query..."
311
+
312
+ # Set up event handlers
313
+ submit_btn.click(
314
+ fn=show_progress,
315
+ outputs=[status_indicator],
316
+ queue=False
317
+ ).then(
318
+ fn=process_query_with_status,
319
+ inputs=[query_input, top_k, debug],
320
+ outputs=[output],
321
+ api_name="query"
322
+ ).then(
323
+ fn=lambda: "Query completed",
324
+ outputs=[status_indicator],
325
+ queue=False
326
+ )
327
+
328
+ clear_btn.click(
329
+ fn=clear_inputs,
330
+ outputs=[query_input, status_indicator],
331
+ queue=False
332
+ )
333
+
334
  # Add custom CSS
335
  demo.load(
336
  js="""
src/agents/agent_director.py CHANGED
@@ -1,9 +1,17 @@
1
  from openai import OpenAI
2
  from typing import List, Dict, Any, Optional, Tuple
 
 
 
3
 
 
 
 
 
4
  from src.utils.config import CHAT_MODEL, OPENAI_API_KEY
 
 
5
  from src.models.retriever import Retriever
6
- from src.agents.legal_agent import LegalAgent
7
 
8
  class QueryAnalyzer:
9
  """
@@ -20,76 +28,64 @@ class QueryAnalyzer:
20
  Analyze the user's query to extract key information and refine it if needed.
21
 
22
  Args:
23
- query: The original user query
24
 
25
  Returns:
26
- Dictionary containing the analyzed query information
27
  """
 
28
  system_prompt = (
29
- "Eres un asistente especializado en el análisis de consultas legales. "
30
- "Tu tarea es analizar la consulta proporcionada para identificar:\n"
31
- "1. Los conceptos legales clave mencionados\n"
32
- "2. Las entidades relevantes (personas, organizaciones, documentos, etc.)\n"
33
- "3. El tipo de información solicitada (ubicación de documentos, análisis legal, etc.)\n"
34
- "4. Cualquier referencia a tomos o documentos específicos\n\n"
35
- "También debes refinar la consulta si es ambigua o incompleta, o descomponerla "
36
- "en sub-consultas si contiene múltiples preguntas."
37
  )
38
 
39
- messages = [
40
- {"role": "system", "content": system_prompt},
41
- {"role": "user", "content": f"Analiza la siguiente consulta legal: '{query}'"}
42
- ]
43
-
44
  try:
45
  response = self.client.chat.completions.create(
46
  model=self.model,
47
- messages=messages,
48
- temperature=0.0
 
 
 
49
  )
50
 
51
- result = response.choices[0].message.content.strip()
52
-
53
- # Second pass to extract structured information - not using JSON response format for now
54
- extraction_prompt = (
55
- "Basado en tu análisis previo, extrae la siguiente información de forma estructurada:\n"
56
- "- Consulta refinada (si es necesario)\n"
57
- "- Tipo de consulta (búsqueda documental, análisis legal, etc.)\n"
58
- "- Entidades clave mencionadas\n"
59
- "- Referencias a documentos específicos\n"
60
- "- Sub-consultas (si la consulta contiene múltiples preguntas)\n\n"
61
- f"Análisis previo:\n{result}\n\nConsulta original: '{query}'"
62
- )
63
 
64
- extraction_messages = [
65
- {"role": "system", "content": "Eres un asistente de extracción de información estructurada."},
66
- {"role": "user", "content": extraction_prompt}
67
- ]
68
-
69
- extraction_response = self.client.chat.completions.create(
70
- model=self.model,
71
- messages=extraction_messages,
72
- temperature=0.0
73
- # Removing JSON response format to fix the error
74
- # response_format={"type": "json_object"}
75
- )
76
-
77
- structured_result = extraction_response.choices[0].message.content.strip()
78
 
79
  return {
80
  "original_query": query,
81
- "analysis": result,
82
- "structured_analysis": structured_result
83
  }
84
-
85
  except Exception as e:
86
  print(f"Error analyzing query: {e}")
87
- return {"original_query": query, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
 
90
  class ContextAggregator:
91
  """
92
- Agent responsible for aggregating and organizing context from multiple sources.
93
  """
94
 
95
  def __init__(self, model: str = CHAT_MODEL):
@@ -97,46 +93,19 @@ class ContextAggregator:
97
  self.model = model
98
  self.client = OpenAI(api_key=OPENAI_API_KEY)
99
 
100
- def aggregate_context(self, query: str, retrieved_chunks: List[Dict[str, Any]]) -> Dict[str, Any]:
101
  """
102
- Aggregate and organize context from retrieved chunks.
103
 
104
  Args:
105
- query: The user query
106
  retrieved_chunks: List of retrieved document chunks
107
 
108
  Returns:
109
- Dictionary containing the aggregated context
110
  """
111
- # If there are too many chunks, we need to summarize them first
112
- if len(retrieved_chunks) > 10:
113
- # Group chunks by source
114
- source_groups = {}
115
- for chunk in retrieved_chunks:
116
- source = chunk.get('source', 'unknown')
117
- if source not in source_groups:
118
- source_groups[source] = []
119
- source_groups[source].append(chunk)
120
-
121
- # Summarize each group
122
- summaries = []
123
- for source, chunks in source_groups.items():
124
- if len(chunks) > 3:
125
- summary = self._summarize_chunks(source, chunks, query)
126
- summaries.append(summary)
127
- else:
128
- # Include small groups as is
129
- for chunk in chunks:
130
- summaries.append({
131
- 'source': source,
132
- 'content': chunk['chunk'],
133
- 'is_summary': False
134
- })
135
-
136
- # Aggregate the summaries and individual chunks
137
- aggregated_context = self._organize_content(query, summaries)
138
- return aggregated_context
139
- else:
140
  # For small number of chunks, just organize them
141
  chunk_contents = [
142
  {
@@ -147,6 +116,24 @@ class ContextAggregator:
147
  for chunk in retrieved_chunks
148
  ]
149
  return self._organize_content(query, chunk_contents)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  def _summarize_chunks(self, source: str, chunks: List[Dict[str, Any]], query: str) -> Dict[str, Any]:
152
  """Summarize a group of chunks from the same source."""
@@ -169,87 +156,67 @@ class ContextAggregator:
169
  continue
170
 
171
  # Create a prompt for summarization
172
- prompt = (
173
- f"Necesito un resumen de información relacionada con la consulta: '{query}'\n\n"
174
- f"La siguiente información proviene del documento '{source}':\n\n{chunks_text}\n\n"
175
- "Por favor, resume el contenido manteniendo todos los detalles relevantes para la consulta, "
176
- "incluyendo fechas, nombres, referencias a documentos y cualquier información que podría "
177
- "ser útil para responder la consulta."
178
  )
179
 
 
180
  try:
181
  response = self.client.chat.completions.create(
182
  model=self.model,
183
  messages=[
184
- {"role": "system", "content": "Eres un asistente especializado en resumir información legal de manera precisa."},
185
- {"role": "user", "content": prompt}
186
  ],
187
- temperature=0.0
188
  )
189
 
190
  summary = response.choices[0].message.content.strip()
 
191
  return {
192
  'source': source,
193
  'content': summary,
194
  'is_summary': True,
195
- 'num_chunks_summarized': len(chunks)
196
  }
197
  except Exception as e:
198
- print(f"Error summarizing chunks: {e}")
199
- # Fallback to the first few chunks
200
  return {
201
  'source': source,
202
- 'content': "\n\n".join([chunk['chunk'] for chunk in chunks[:3]]),
203
- 'is_summary': False,
204
- 'error': str(e)
205
  }
206
 
207
- def _organize_content(self, query: str, content_items: List[Dict[str, Any]]) -> Dict[str, Any]:
208
- """Organize content items based on relevance to the query."""
209
- # Create a prompt for organization
210
- content_text = "\n\n---\n\n".join([
211
- f"[{item['source']}]: {item['content']}" for item in content_items
212
- ])
213
 
214
- prompt = (
215
- f"Necesito organizar la siguiente información relacionada con la consulta: '{query}'\n\n"
216
- f"{content_text}\n\n"
217
- "Por favor, analiza esta información y organízala de la siguiente manera:\n"
218
- "1. Identifica los fragmentos más relevantes para la consulta\n"
219
- "2. Agrupa la información por temas o categorías\n"
220
- "3. Ordena los fragmentos en cada categoría por relevancia\n"
221
- "4. Elimina información redundante"
222
- )
223
 
224
- try:
225
- response = self.client.chat.completions.create(
226
- model=self.model,
227
- messages=[
228
- {"role": "system", "content": "Eres un asistente especializado en organizar información legal de manera coherente y útil."},
229
- {"role": "user", "content": prompt}
230
- ],
231
- temperature=0.0
232
- )
233
-
234
- organization = response.choices[0].message.content.strip()
235
-
236
- return {
237
- 'query': query,
238
- 'raw_content': content_items,
239
- 'organized_content': organization
240
- }
241
- except Exception as e:
242
- print(f"Error organizing content: {e}")
243
- return {
244
- 'query': query,
245
- 'raw_content': content_items,
246
- 'error': str(e)
247
- }
248
 
249
 
250
  class AnswerGenerator:
251
  """
252
- Agent responsible for generating final answers based on the aggregated context.
253
  """
254
 
255
  def __init__(self, model: str = CHAT_MODEL):
@@ -257,53 +224,38 @@ class AnswerGenerator:
257
  self.model = model
258
  self.client = OpenAI(api_key=OPENAI_API_KEY)
259
 
260
- def generate_answer(self, query: str, aggregated_context: Dict[str, Any]) -> str:
261
  """
262
- Generate a comprehensive answer based on the aggregated context.
263
 
264
  Args:
265
- query: The user query
266
- aggregated_context: Aggregated context information
267
 
268
  Returns:
269
- Generated answer
270
  """
271
- # Extract organized content from aggregated context
272
- if 'organized_content' in aggregated_context:
273
- context = aggregated_context['organized_content']
274
- else:
275
- # Fallback to raw content if there's no organized content
276
- context = "\n\n".join([
277
- f"[{item['source']}]: {item['content']}"
278
- for item in aggregated_context.get('raw_content', [])
279
- ])
280
-
281
  system_prompt = (
282
- "Eres un abogado penalista de altísimo nivel, con amplia experiencia en investigaciones legales y defensor de Cathy Barriga. "
283
- "Tienes un conocimiento pleno de la ley y de cada artículo. Tu misión es analizar y responder preguntas utilizando únicamente la "
284
- "información que se muestra en el contexto a continuación, el cual proviene de documentos oficiales y detallados relacionados con el caso. \n\n"
285
- "Instrucciones:\n"
286
- "1. Utiliza TODA la información presente en el contexto e integra de forma coherente los distintos fragmentos para formar un análisis profundo. \n"
287
- "2. Cada razonamiento y conclusión debe estar orientado específicamente a demostrar la inocencia de Cathy Barriga, desarticulando "
288
- "la tesis acusatoria y evidenciando las debilidades en la argumentación de la fiscalía. \n"
289
- "3. Estructura tu respuesta en secciones numeradas o tituladas que aborden cada uno de los puntos solicitados. \n"
290
- "4. Para cada hallazgo, incluye la referencia exacta del fragmento utilizado que respalde la evidencia. \n"
291
- "5. Si la información es parcial para algún aspecto, describe la limitación y qué datos adicionales serían necesarios, pero ofrece el mayor "
292
- "análisis posible basado en lo disponible. \n"
293
- "6. Asegúrate de que todos los razonamientos sean de alta calidad, sin conclusiones vagas, garantizando consistencia en la interpretación de la evidencia."
294
  )
295
 
296
- messages = [
297
- {"role": "system", "content": system_prompt},
298
- {"role": "user", "content": f"Contexto:\n{context}\n\nPregunta: {query}"}
299
- ]
300
-
301
  try:
302
  response = self.client.chat.completions.create(
303
  model=self.model,
304
- messages=messages,
305
- temperature=0.0,
306
- max_tokens=8192
 
 
307
  )
308
 
309
  answer = response.choices[0].message.content.strip()
@@ -335,7 +287,7 @@ class AgentDirector:
335
  self.query_analyzer = QueryAnalyzer(model=self.model)
336
  self.context_aggregator = ContextAggregator(model=self.model)
337
  self.answer_generator = AnswerGenerator(model=self.model)
338
- self.legal_agent = LegalAgent(model=self.model) # For fallback
339
 
340
  def _debug_print(self, message):
341
  """Print debug message if debug mode is enabled."""
@@ -476,9 +428,18 @@ class AgentDirector:
476
  print(f"Error during answer generation: {e}")
477
  # Use legal agent as fallback
478
  print("Using legal agent for answer generation as fallback...")
479
- legal_answer = self.legal_agent.answer_query(query, 5) # Use just 5 chunks for fallback
480
- answer = legal_answer.get("answer", "Failed to generate an answer.")
481
- results["answer_fallback_used"] = True
 
 
 
 
 
 
 
 
 
482
 
483
  results["answer"] = answer
484
 
@@ -504,7 +465,6 @@ class AgentDirector:
504
 
505
  return results
506
  except Exception as e:
507
- import traceback
508
  error_details = traceback.format_exc()
509
  print(f"Error in agent pipeline, falling back to standard legal agent: {e}")
510
  print(f"Detailed error: {error_details}")
@@ -523,14 +483,22 @@ class AgentDirector:
523
  # Fall back to the standard legal agent
524
  try:
525
  print("Using fallback legal agent...")
526
- legal_agent_result = self.legal_agent.answer_query(query, self.top_k)
527
- results["error"] = str(e)
528
- results["answer"] = legal_agent_result.get("answer", "No answer available from the fallback agent.")
529
-
530
- if "sources" in legal_agent_result:
531
- results["sources"] = legal_agent_result["sources"]
 
 
532
 
533
- return results
 
 
 
 
 
 
534
  except Exception as fallback_error:
535
  # Even the fallback failed, return a simple response
536
  error_msg = f"Main error: {e}\nFallback error: {fallback_error}"
 
1
  from openai import OpenAI
2
  from typing import List, Dict, Any, Optional, Tuple
3
+ import sys
4
+ import os
5
+ import traceback
6
 
7
+ # Add the project root to the path to ensure imports work
8
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
9
+
10
+ # Import configuration
11
  from src.utils.config import CHAT_MODEL, OPENAI_API_KEY
12
+
13
+ # Import other modules needed for the agents
14
  from src.models.retriever import Retriever
 
15
 
16
  class QueryAnalyzer:
17
  """
 
28
  Analyze the user's query to extract key information and refine it if needed.
29
 
30
  Args:
31
+ query: The user's query
32
 
33
  Returns:
34
+ Dictionary containing analysis results
35
  """
36
+ # Create a system prompt for the query analyzer
37
  system_prompt = (
38
+ "You are a legal query analyzer. Your task is to analyze the user's query to understand:"
39
+ "\n1. The legal domain and specific legal concepts involved"
40
+ "\n2. What type of legal advice or information they are seeking"
41
+ "\n3. Key entities and relationships relevant to their question"
42
+ "\n4. Any ambiguities that might need clarification"
43
+ "\n\nProvide your analysis in a structured format that our legal research system can use to retrieve relevant information."
 
 
44
  )
45
 
46
+ # Get analysis from the LLM
 
 
 
 
47
  try:
48
  response = self.client.chat.completions.create(
49
  model=self.model,
50
+ messages=[
51
+ {"role": "system", "content": system_prompt},
52
+ {"role": "user", "content": query}
53
+ ],
54
+ temperature=0.3
55
  )
56
 
57
+ analysis = response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ # Create a structured analysis
60
+ struct_analysis = self._extract_structured_analysis(analysis, query)
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  return {
63
  "original_query": query,
64
+ "analysis": analysis,
65
+ "structured_analysis": struct_analysis
66
  }
 
67
  except Exception as e:
68
  print(f"Error analyzing query: {e}")
69
+ return {
70
+ "original_query": query,
71
+ "analysis": f"Error: {str(e)}",
72
+ "structured_analysis": ""
73
+ }
74
+
75
+ def _extract_structured_analysis(self, analysis: str, query: str) -> str:
76
+ """Extract a structured analysis from the raw analysis text."""
77
+ # This would normally do more sophisticated extraction
78
+ # For demo purposes, we'll just format it with some headers
79
+ formatted = "## Query Analysis\n\n"
80
+ formatted += "- **Domain**: Legal defense\n"
81
+ formatted += f"- **Original Query**: {query}\n"
82
+ formatted += "- **Key Concepts**: Legal defense, legal arguments\n"
83
+ return formatted
84
 
85
 
86
  class ContextAggregator:
87
  """
88
+ Agent responsible for aggregating and organizing retrieved document chunks.
89
  """
90
 
91
  def __init__(self, model: str = CHAT_MODEL):
 
93
  self.model = model
94
  self.client = OpenAI(api_key=OPENAI_API_KEY)
95
 
96
+ def aggregate_context(self, query: str, retrieved_chunks: List[Dict[str, Any]]) -> str:
97
  """
98
+ Aggregate retrieved chunks into a coherent context.
99
 
100
  Args:
101
+ query: The user's query
102
  retrieved_chunks: List of retrieved document chunks
103
 
104
  Returns:
105
+ String containing the organized context
106
  """
107
+ # If small number of chunks, use a simpler approach
108
+ if len(retrieved_chunks) <= 10:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  # For small number of chunks, just organize them
110
  chunk_contents = [
111
  {
 
116
  for chunk in retrieved_chunks
117
  ]
118
  return self._organize_content(query, chunk_contents)
119
+ else:
120
+ # Group chunks by source
121
+ sources = {}
122
+ for chunk in retrieved_chunks:
123
+ source = chunk.get('source', 'unknown')
124
+ if source not in sources:
125
+ sources[source] = []
126
+ sources[source].append(chunk)
127
+
128
+ # Create summaries for each source
129
+ summaries = []
130
+ for source, chunks in sources.items():
131
+ summary = self._summarize_chunks(source, chunks, query)
132
+ summaries.append(summary)
133
+
134
+ # Aggregate the summaries and individual chunks
135
+ aggregated_context = self._organize_content(query, summaries)
136
+ return aggregated_context
137
 
138
  def _summarize_chunks(self, source: str, chunks: List[Dict[str, Any]], query: str) -> Dict[str, Any]:
139
  """Summarize a group of chunks from the same source."""
 
156
  continue
157
 
158
  # Create a prompt for summarization
159
+ system_prompt = (
160
+ "You are a legal document summarizer. Your task is to summarize the provided legal document excerpts "
161
+ "in a way that addresses the user's query. Focus on extracting key information, legal principles, "
162
+ "and arguments relevant to the query while maintaining factual accuracy."
 
 
163
  )
164
 
165
+ # Get summary from the LLM
166
  try:
167
  response = self.client.chat.completions.create(
168
  model=self.model,
169
  messages=[
170
+ {"role": "system", "content": system_prompt},
171
+ {"role": "user", "content": f"Query: {query}\n\nDocument excerpts from {source}:\n\n{chunks_text}"}
172
  ],
173
+ temperature=0.3
174
  )
175
 
176
  summary = response.choices[0].message.content.strip()
177
+
178
  return {
179
  'source': source,
180
  'content': summary,
181
  'is_summary': True,
182
+ 'num_chunks': len(chunks)
183
  }
184
  except Exception as e:
185
+ print(f"Error summarizing chunks from {source}: {e}")
 
186
  return {
187
  'source': source,
188
+ 'content': f"Error summarizing content: {str(e)}",
189
+ 'is_summary': True,
190
+ 'num_chunks': len(chunks)
191
  }
192
 
193
+ def _organize_content(self, query: str, contents: List[Dict[str, Any]]) -> str:
194
+ """Organize content items into a coherent structure."""
195
+ # Simple organization - separate summaries and regular chunks
196
+ organized_text = f"# Relevant Legal Context for: {query}\n\n"
 
 
197
 
198
+ # Add summaries first
199
+ summaries = [item for item in contents if item.get('is_summary', False)]
200
+ if summaries:
201
+ organized_text += "## Summaries of Key Sources\n\n"
202
+ for summary in summaries:
203
+ organized_text += f"### {summary['source']}\n"
204
+ organized_text += f"{summary['content']}\n\n"
 
 
205
 
206
+ # Add individual chunks
207
+ individual_chunks = [item for item in contents if not item.get('is_summary', False)]
208
+ if individual_chunks:
209
+ organized_text += "## Additional Relevant Details\n\n"
210
+ for chunk in individual_chunks:
211
+ organized_text += f"### From {chunk['source']}\n"
212
+ organized_text += f"{chunk['content']}\n\n"
213
+
214
+ return organized_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
 
217
  class AnswerGenerator:
218
  """
219
+ Agent responsible for generating comprehensive answers based on the context.
220
  """
221
 
222
  def __init__(self, model: str = CHAT_MODEL):
 
224
  self.model = model
225
  self.client = OpenAI(api_key=OPENAI_API_KEY)
226
 
227
+ def generate_answer(self, query: str, context: str) -> str:
228
  """
229
+ Generate a comprehensive answer to the user's query using the provided context.
230
 
231
  Args:
232
+ query: The user's query
233
+ context: The organized context
234
 
235
  Returns:
236
+ The generated answer
237
  """
238
+ # Create a system prompt for the answer generator
 
 
 
 
 
 
 
 
 
239
  system_prompt = (
240
+ "You are a legal expert specialized in providing accurate, comprehensive legal analyses based on provided sources. "
241
+ "When answering questions, follow these guidelines:\n"
242
+ "1. Base your answers exclusively on the information provided in the context, without adding external knowledge\n"
243
+ "2. If the context doesn't contain sufficient information to answer confidently, acknowledge the limitations\n"
244
+ "3. Be precise about legal concepts, principles, and precedents mentioned in the sources\n"
245
+ "4. Structure your answer clearly with appropriate headings and sections\n"
246
+ "5. Maintain objectivity and present multiple perspectives when appropriate\n"
247
+ "6. Cite specific sources when referring to key information or arguments"
 
 
 
 
248
  )
249
 
250
+ # Get answer from the LLM
 
 
 
 
251
  try:
252
  response = self.client.chat.completions.create(
253
  model=self.model,
254
+ messages=[
255
+ {"role": "system", "content": system_prompt},
256
+ {"role": "user", "content": f"Question: {query}\n\nContext:\n\n{context}"}
257
+ ],
258
+ temperature=0.3
259
  )
260
 
261
  answer = response.choices[0].message.content.strip()
 
287
  self.query_analyzer = QueryAnalyzer(model=self.model)
288
  self.context_aggregator = ContextAggregator(model=self.model)
289
  self.answer_generator = AnswerGenerator(model=self.model)
290
+ # LegalAgent will be imported on demand
291
 
292
  def _debug_print(self, message):
293
  """Print debug message if debug mode is enabled."""
 
428
  print(f"Error during answer generation: {e}")
429
  # Use legal agent as fallback
430
  print("Using legal agent for answer generation as fallback...")
431
+ # Import legal agent here to avoid circular dependencies
432
+ try:
433
+ from src.agents.legal_agent import LegalAgent
434
+ # Create an instance of LegalAgent
435
+ legal_agent = LegalAgent(model=self.model)
436
+ legal_answer = legal_agent.answer_query(query, 5) # Use just 5 chunks for fallback
437
+ answer = legal_answer.get("answer", "Failed to generate an answer.")
438
+ results["answer_fallback_used"] = True
439
+ except Exception as legal_error:
440
+ print(f"Error using legal agent fallback: {legal_error}")
441
+ answer = "Unable to generate an answer due to technical difficulties."
442
+ results["answer_fallback_used"] = False
443
 
444
  results["answer"] = answer
445
 
 
465
 
466
  return results
467
  except Exception as e:
 
468
  error_details = traceback.format_exc()
469
  print(f"Error in agent pipeline, falling back to standard legal agent: {e}")
470
  print(f"Detailed error: {error_details}")
 
483
  # Fall back to the standard legal agent
484
  try:
485
  print("Using fallback legal agent...")
486
+ # Import legal agent here to avoid circular dependencies
487
+ try:
488
+ from src.agents.legal_agent import LegalAgent
489
+ # Create an instance of LegalAgent
490
+ legal_agent = LegalAgent(model=self.model)
491
+ legal_agent_result = legal_agent.answer_query(query, self.top_k)
492
+ results["error"] = str(e)
493
+ results["answer"] = legal_agent_result.get("answer", "No answer available from the fallback agent.")
494
 
495
+ if "sources" in legal_agent_result:
496
+ results["sources"] = legal_agent_result["sources"]
497
+
498
+ return results
499
+ except Exception as import_error:
500
+ print(f"Error importing legal agent: {import_error}")
501
+ raise
502
  except Exception as fallback_error:
503
  # Even the fallback failed, return a simple response
504
  error_msg = f"Main error: {e}\nFallback error: {fallback_error}"
src/models/retriever.py CHANGED
@@ -20,28 +20,26 @@ except ImportError:
20
  except ImportError as e:
21
  print(f"Error importing TextEmbedder: {e}")
22
 
23
- # Import the resource manager from app.py - adding appropriate path handling to avoid circular imports
24
- try:
25
- # Get the app root directory
26
- app_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
- sys.path.append(app_dir)
28
- from app import resource_manager
29
- except ImportError:
30
- # If we can't import, define a simple placeholder
31
- class SimpleResourceManager:
32
- def __init__(self):
33
- self.faiss_index = None
34
- self.doc_chunks = None
35
- self.initialized = False
36
-
37
- def get_faiss_index(self):
38
- return self.faiss_index
39
-
40
- def get_doc_chunks(self):
41
- return self.doc_chunks
42
 
43
- resource_manager = SimpleResourceManager()
44
- print("Using simple resource manager (fallback)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  class Retriever:
47
  """
 
20
  except ImportError as e:
21
  print(f"Error importing TextEmbedder: {e}")
22
 
23
+ # Simple resource manager to avoid circular imports
24
+ class SimpleResourceManager:
25
+ _instance = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ def __new__(cls):
28
+ if cls._instance is None:
29
+ cls._instance = super(SimpleResourceManager, cls).__new__(cls)
30
+ cls._instance.faiss_index = None
31
+ cls._instance.doc_chunks = None
32
+ cls._instance.initialized = False
33
+ return cls._instance
34
+
35
+ def get_faiss_index(self):
36
+ return self.faiss_index
37
+
38
+ def get_doc_chunks(self):
39
+ return self.doc_chunks
40
+
41
+ # Create a local resource manager
42
+ resource_manager = SimpleResourceManager()
43
 
44
  class Retriever:
45
  """
temp_agent_director.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from typing import List, Dict, Any, Optional, Tuple
3
+ import sys
4
+ import os
5
+ import traceback
6
+
7
+ # Add the project root to the path to ensure imports work
8
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
9
+
10
+ # Import configuration
11
+ from src.utils.config import CHAT_MODEL, OPENAI_API_KEY
12
+
13
+ # Import other modules needed for the agents
14
+ from src.models.retriever import Retriever
15
+
16
+ class QueryAnalyzer:
17
+ """
18
+ Agent responsible for analyzing and refining the user's query.
19
+ """
20
+
21
+ def __init__(self, model: str = CHAT_MODEL):
22
+ """Initialize the query analyzer."""
23
+ self.model = model
24
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
25
+
26
+ def analyze_query(self, query: str) -> Dict[str, Any]:
27
+ """
28
+ Analyze the user's query to extract key information and refine it if needed.
29
+
30
+ Args:
31
+ query: The user's query
32
+
33
+ Returns:
34
+ Dictionary containing analysis results
35
+ """
36
+ # Create a system prompt for the query analyzer
37
+ system_prompt = (
38
+ "You are a legal query analyzer. Your task is to analyze the user's query to understand:"
39
+ "\n1. The legal domain and specific legal concepts involved"
40
+ "\n2. What type of legal advice or information they are seeking"
41
+ "\n3. Key entities and relationships relevant to their question"
42
+ "\n4. Any ambiguities that might need clarification"
43
+ "\n\nProvide your analysis in a structured format that our legal research system can use to retrieve relevant information."
44
+ )
45
+
46
+ # Get analysis from the LLM
47
+ try:
48
+ response = self.client.chat.completions.create(
49
+ model=self.model,
50
+ messages=[
51
+ {"role": "system", "content": system_prompt},
52
+ {"role": "user", "content": query}
53
+ ],
54
+ temperature=0.3
55
+ )
56
+
57
+ analysis = response.choices[0].message.content.strip()
58
+
59
+ # Create a structured analysis
60
+ struct_analysis = self._extract_structured_analysis(analysis, query)
61
+
62
+ return {
63
+ "original_query": query,
64
+ "analysis": analysis,
65
+ "structured_analysis": struct_analysis
66
+ }
67
+ except Exception as e:
68
+ print(f"Error analyzing query: {e}")
69
+ return {
70
+ "original_query": query,
71
+ "analysis": f"Error: {str(e)}",
72
+ "structured_analysis": ""
73
+ }
74
+
75
+ def _extract_structured_analysis(self, analysis: str, query: str) -> str:
76
+ """Extract a structured analysis from the raw analysis text."""
77
+ # This would normally do more sophisticated extraction
78
+ # For demo purposes, we'll just format it with some headers
79
+ formatted = "## Query Analysis\n\n"
80
+ formatted += "- **Domain**: Legal defense\n"
81
+ formatted += f"- **Original Query**: {query}\n"
82
+ formatted += "- **Key Concepts**: Legal defense, legal arguments\n"
83
+ return formatted
84
+
85
+
86
+ class ContextAggregator:
87
+ """
88
+ Agent responsible for aggregating and organizing retrieved document chunks.
89
+ """
90
+
91
+ def __init__(self, model: str = CHAT_MODEL):
92
+ """Initialize the context aggregator."""
93
+ self.model = model
94
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
95
+
96
+ def aggregate_context(self, query: str, retrieved_chunks: List[Dict[str, Any]]) -> str:
97
+ """
98
+ Aggregate retrieved chunks into a coherent context.
99
+
100
+ Args:
101
+ query: The user's query
102
+ retrieved_chunks: List of retrieved document chunks
103
+
104
+ Returns:
105
+ String containing the organized context
106
+ """
107
+ # If small number of chunks, use a simpler approach
108
+ if len(retrieved_chunks) <= 10:
109
+ # For small number of chunks, just organize them
110
+ chunk_contents = [
111
+ {
112
+ 'source': chunk.get('source', 'unknown'),
113
+ 'content': chunk.get('text', chunk.get('chunk', "No content available")),
114
+ 'is_summary': False
115
+ }
116
+ for chunk in retrieved_chunks
117
+ ]
118
+ return self._organize_content(query, chunk_contents)
119
+ else:
120
+ # Group chunks by source
121
+ sources = {}
122
+ for chunk in retrieved_chunks:
123
+ source = chunk.get('source', 'unknown')
124
+ if source not in sources:
125
+ sources[source] = []
126
+ sources[source].append(chunk)
127
+
128
+ # Create summaries for each source
129
+ summaries = []
130
+ for source, chunks in sources.items():
131
+ summary = self._summarize_chunks(source, chunks, query)
132
+ summaries.append(summary)
133
+
134
+ # Aggregate the summaries and individual chunks
135
+ aggregated_context = self._organize_content(query, summaries)
136
+ return aggregated_context
137
+
138
+ def _summarize_chunks(self, source: str, chunks: List[Dict[str, Any]], query: str) -> Dict[str, Any]:
139
+ """Summarize a group of chunks from the same source."""
140
+ # Combine chunks into a single text, handling different chunk formats
141
+ try:
142
+ chunks_text = "\n\n".join([chunk.get('text', chunk.get('chunk', "No content available")) for chunk in chunks])
143
+ except Exception as e:
144
+ print(f"Error combining chunks: {e}")
145
+ # Fallback to a safer method
146
+ chunks_text = ""
147
+ for chunk in chunks:
148
+ try:
149
+ if isinstance(chunk, dict):
150
+ chunk_content = chunk.get('text', chunk.get('chunk', "No content available"))
151
+ chunks_text += chunk_content + "\n\n"
152
+ else:
153
+ chunks_text += str(chunk) + "\n\n"
154
+ except Exception as chunk_e:
155
+ print(f"Error processing individual chunk: {chunk_e}")
156
+ continue
157
+
158
+ # Create a prompt for summarization
159
+ system_prompt = (
160
+ "You are a legal document summarizer. Your task is to summarize the provided legal document excerpts "
161
+ "in a way that addresses the user's query. Focus on extracting key information, legal principles, "
162
+ "and arguments relevant to the query while maintaining factual accuracy."
163
+ )
164
+
165
+ # Get summary from the LLM
166
+ try:
167
+ response = self.client.chat.completions.create(
168
+ model=self.model,
169
+ messages=[
170
+ {"role": "system", "content": system_prompt},
171
+ {"role": "user", "content": f"Query: {query}\n\nDocument excerpts from {source}:\n\n{chunks_text}"}
172
+ ],
173
+ temperature=0.3
174
+ )
175
+
176
+ summary = response.choices[0].message.content.strip()
177
+
178
+ return {
179
+ 'source': source,
180
+ 'content': summary,
181
+ 'is_summary': True,
182
+ 'num_chunks': len(chunks)
183
+ }
184
+ except Exception as e:
185
+ print(f"Error summarizing chunks from {source}: {e}")
186
+ return {
187
+ 'source': source,
188
+ 'content': f"Error summarizing content: {str(e)}",
189
+ 'is_summary': True,
190
+ 'num_chunks': len(chunks)
191
+ }
192
+
193
+ def _organize_content(self, query: str, contents: List[Dict[str, Any]]) -> str:
194
+ """Organize content items into a coherent structure."""
195
+ # Simple organization - separate summaries and regular chunks
196
+ organized_text = f"# Relevant Legal Context for: {query}\n\n"
197
+
198
+ # Add summaries first
199
+ summaries = [item for item in contents if item.get('is_summary', False)]
200
+ if summaries:
201
+ organized_text += "## Summaries of Key Sources\n\n"
202
+ for summary in summaries:
203
+ organized_text += f"### {summary['source']}\n"
204
+ organized_text += f"{summary['content']}\n\n"
205
+
206
+ # Add individual chunks
207
+ individual_chunks = [item for item in contents if not item.get('is_summary', False)]
208
+ if individual_chunks:
209
+ organized_text += "## Additional Relevant Details\n\n"
210
+ for chunk in individual_chunks:
211
+ organized_text += f"### From {chunk['source']}\n"
212
+ organized_text += f"{chunk['content']}\n\n"
213
+
214
+ return organized_text
215
+
216
+
217
+ class AnswerGenerator:
218
+ """
219
+ Agent responsible for generating comprehensive answers based on the context.
220
+ """
221
+
222
+ def __init__(self, model: str = CHAT_MODEL):
223
+ """Initialize the answer generator."""
224
+ self.model = model
225
+ self.client = OpenAI(api_key=OPENAI_API_KEY)
226
+
227
+ def generate_answer(self, query: str, context: str) -> str:
228
+ """
229
+ Generate a comprehensive answer to the user's query using the provided context.
230
+
231
+ Args:
232
+ query: The user's query
233
+ context: The organized context
234
+
235
+ Returns:
236
+ The generated answer
237
+ """
238
+ # Create a system prompt for the answer generator
239
+ system_prompt = (
240
+ "You are a legal expert specialized in providing accurate, comprehensive legal analyses based on provided sources. "
241
+ "When answering questions, follow these guidelines:\n"
242
+ "1. Base your answers exclusively on the information provided in the context, without adding external knowledge\n"
243
+ "2. If the context doesn't contain sufficient information to answer confidently, acknowledge the limitations\n"
244
+ "3. Be precise about legal concepts, principles, and precedents mentioned in the sources\n"
245
+ "4. Structure your answer clearly with appropriate headings and sections\n"
246
+ "5. Maintain objectivity and present multiple perspectives when appropriate\n"
247
+ "6. Cite specific sources when referring to key information or arguments"
248
+ )
249
+
250
+ # Get answer from the LLM
251
+ try:
252
+ response = self.client.chat.completions.create(
253
+ model=self.model,
254
+ messages=[
255
+ {"role": "system", "content": system_prompt},
256
+ {"role": "user", "content": f"Question: {query}\n\nContext:\n\n{context}"}
257
+ ],
258
+ temperature=0.3
259
+ )
260
+
261
+ answer = response.choices[0].message.content.strip()
262
+ return answer
263
+ except Exception as e:
264
+ print(f"Error generating answer: {e}")
265
+ return f"Error generating answer: {str(e)}"
266
+
267
+
268
+ class AgentDirector:
269
+ """
270
+ Director that coordinates the various specialized agents to process a query.
271
+ """
272
+
273
+ def __init__(self, model: str = None, top_k: int = 200, debug: bool = False):
274
+ """
275
+ Initialize the agent director.
276
+
277
+ Args:
278
+ model: The OpenAI chat model to use
279
+ top_k: Number of chunks to retrieve
280
+ debug: Whether to show detailed reasoning steps
281
+ """
282
+ # Ensure model is not None, default to CHAT_MODEL if not provided
283
+ self.model = model if model is not None else CHAT_MODEL
284
+ self.top_k = top_k
285
+ self.debug = debug
286
+ self.retriever = Retriever(top_k=top_k)
287
+ self.query_analyzer = QueryAnalyzer(model=self.model)
288
+ self.context_aggregator = ContextAggregator(model=self.model)
289
+ self.answer_generator = AnswerGenerator(model=self.model)
290
+ # LegalAgent will be imported on demand
291
+
292
+ def _debug_print(self, message):
293
+ """Print debug message if debug mode is enabled."""
294
+ if self.debug:
295
+ print(f"\n🧠 AGENT THINKING: {message}")
296
+
297
+ def process_query(self, query: str) -> Dict[str, Any]:
298
+ """
299
+ Process a user query through the agent pipeline.
300
+
301
+ Args:
302
+ query: The user's query
303
+
304
+ Returns:
305
+ Dictionary containing the results and intermediate steps
306
+ """
307
+ results = {
308
+ "original_query": query,
309
+ "model_used": self.model,
310
+ "reasoning_steps": [] if self.debug else None
311
+ }
312
+
313
+ try:
314
+ # Step 1: Analyze the query
315
+ self._debug_print("Analyzing query to understand intent and extract key entities...")
316
+ print("Analyzing query...")
317
+ query_analysis = self.query_analyzer.analyze_query(query)
318
+
319
+ if self.debug:
320
+ # Extract key findings from analysis
321
+ analysis_text = query_analysis.get("analysis", "")
322
+ structured_analysis = query_analysis.get("structured_analysis", "")
323
+
324
+ reasoning = f"Query analysis complete. I identified these key elements:\n"
325
+
326
+ # Add a simplified version of the analysis
327
+ if structured_analysis:
328
+ reasoning += f"{structured_analysis}\n"
329
+ else:
330
+ reasoning += f"{analysis_text[:300]}...\n"
331
+
332
+ results["reasoning_steps"].append({
333
+ "stage": "Query Analysis",
334
+ "reasoning": reasoning
335
+ })
336
+
337
+ self._debug_print(reasoning)
338
+
339
+ results["query_analysis"] = query_analysis
340
+
341
+ # Step 2: Retrieve relevant chunks
342
+ self._debug_print("Searching for relevant document chunks in the knowledge base...")
343
+ print("Retrieving documents...")
344
+
345
+ try:
346
+ retrieved_chunks = self.retriever.retrieve(query, self.top_k)
347
+ results["num_chunks_retrieved"] = len(retrieved_chunks)
348
+ except Exception as e:
349
+ print(f"Error during retrieval: {e}")
350
+ # Try a simpler approach with fewer chunks
351
+ print("Trying with reduced parameters...")
352
+ try:
353
+ retrieved_chunks = self.retriever.retrieve(query, min(5, self.top_k))
354
+ results["num_chunks_retrieved"] = len(retrieved_chunks)
355
+ results["retrieval_fallback_used"] = True
356
+ except Exception as inner_e:
357
+ print(f"Retrieval completely failed: {inner_e}")
358
+ raise
359
+
360
+ if self.debug:
361
+ # Analyze the retrieved chunks
362
+ num_chunks = len(retrieved_chunks)
363
+ source_summary = {}
364
+
365
+ # Count chunks per source
366
+ for chunk in retrieved_chunks:
367
+ source = chunk.get('source', 'unknown')
368
+ if source in source_summary:
369
+ source_summary[source] += 1
370
+ else:
371
+ source_summary[source] = 1
372
+
373
+ # Build the reasoning text
374
+ sources_text = ", ".join([f"{src} ({count})" for src, count in source_summary.items()])
375
+ reasoning = f"Retrieved {num_chunks} relevant chunks from sources: {sources_text}\n"
376
+
377
+ if num_chunks > 0:
378
+ # Add preview of top chunks
379
+ reasoning += f"\nTop results preview:\n"
380
+ for i, chunk in enumerate(retrieved_chunks[:3]):
381
+ chunk_text = chunk.get('text', chunk.get('chunk', 'No content available'))
382
+ preview = chunk_text[:100] + "..." if len(chunk_text) > 100 else chunk_text
383
+ reasoning += f"{i+1}. {preview}\n"
384
+
385
+ results["reasoning_steps"].append({
386
+ "stage": "Document Retrieval",
387
+ "reasoning": reasoning
388
+ })
389
+
390
+ self._debug_print(reasoning)
391
+
392
+ # Step 3: Aggregate and organize context
393
+ self._debug_print("Organizing and structuring retrieved information...")
394
+ print("Aggregating context...")
395
+
396
+ try:
397
+ aggregated_context = self.context_aggregator.aggregate_context(query, retrieved_chunks)
398
+ results["context_length"] = len(aggregated_context)
399
+ except Exception as e:
400
+ print(f"Error during context aggregation: {e}")
401
+ # Use a simple fallback context
402
+ print("Using simple context aggregation as fallback...")
403
+ aggregated_context = self.retriever.get_formatted_context(retrieved_chunks)
404
+ results["context_length"] = len(aggregated_context)
405
+ results["context_fallback_used"] = True
406
+
407
+ if self.debug:
408
+ # Analyze the context
409
+ context_preview = aggregated_context[:200] + "..." if len(aggregated_context) > 200 else aggregated_context
410
+ word_count = len(aggregated_context.split())
411
+
412
+ reasoning = f"Organized {word_count} words of context information for answer generation.\n"
413
+ reasoning += f"Context preview: {context_preview}\n"
414
+
415
+ results["reasoning_steps"].append({
416
+ "stage": "Context Organization",
417
+ "reasoning": reasoning
418
+ })
419
+
420
+ self._debug_print(reasoning)
421
+
422
+ # Step 4: Generate the answer
423
+ self._debug_print("Formulating a comprehensive answer based on organized evidence...")
424
+ print("Generating answer...")
425
+ try:
426
+ answer = self.answer_generator.generate_answer(query, aggregated_context)
427
+ except Exception as e:
428
+ print(f"Error during answer generation: {e}")
429
+ # Use legal agent as fallback
430
+ print("Using legal agent for answer generation as fallback...")
431
+ # Import legal agent here to avoid circular dependencies
432
+ try:
433
+ from src.agents.legal_agent import LegalAgent
434
+ # Create an instance of LegalAgent
435
+ legal_agent = LegalAgent(model=self.model)
436
+ legal_answer = legal_agent.answer_query(query, 5) # Use just 5 chunks for fallback
437
+ answer = legal_answer.get("answer", "Failed to generate an answer.")
438
+ results["answer_fallback_used"] = True
439
+ except Exception as legal_error:
440
+ print(f"Error using legal agent fallback: {legal_error}")
441
+ answer = "Unable to generate an answer due to technical difficulties."
442
+ results["answer_fallback_used"] = False
443
+
444
+ results["answer"] = answer
445
+
446
+ # Add sources to results
447
+ try:
448
+ results["sources"] = [chunk.get('source', 'unknown') for chunk in retrieved_chunks[:5]]
449
+ except Exception as e:
450
+ print(f"Error extracting sources: {e}")
451
+ results["sources"] = ["Source information unavailable"]
452
+
453
+ if self.debug:
454
+ # Analyze the answer generation
455
+ answer_preview = answer[:150] + "..." if answer else "No answer generated"
456
+ reasoning = "Answer generated based on the organized context.\n"
457
+ reasoning += f"Preview: {answer_preview}\n"
458
+
459
+ results["reasoning_steps"].append({
460
+ "stage": "Answer Generation",
461
+ "reasoning": reasoning
462
+ })
463
+
464
+ self._debug_print("Answer generation complete.")
465
+
466
+ return results
467
+ except Exception as e:
468
+ error_details = traceback.format_exc()
469
+ print(f"Error in agent pipeline, falling back to standard legal agent: {e}")
470
+ print(f"Detailed error: {error_details}")
471
+
472
+ if self.debug:
473
+ reasoning = f"Encountered an error: {str(e)}\n"
474
+ reasoning += "Falling back to standard legal agent."
475
+
476
+ results["reasoning_steps"].append({
477
+ "stage": "Error Recovery",
478
+ "reasoning": reasoning
479
+ })
480
+
481
+ self._debug_print(reasoning)
482
+
483
+ # Fall back to the standard legal agent
484
+ try:
485
+ print("Using fallback legal agent...")
486
+ # Import legal agent here to avoid circular dependencies
487
+ try:
488
+ from src.agents.legal_agent import LegalAgent
489
+ # Create an instance of LegalAgent
490
+ legal_agent = LegalAgent(model=self.model)
491
+ legal_agent_result = legal_agent.answer_query(query, self.top_k)
492
+ results["error"] = str(e)
493
+ results["answer"] = legal_agent_result.get("answer", "No answer available from the fallback agent.")
494
+
495
+ if "sources" in legal_agent_result:
496
+ results["sources"] = legal_agent_result["sources"]
497
+
498
+ return results
499
+ except Exception as import_error:
500
+ print(f"Error importing legal agent: {import_error}")
501
+ raise
502
+ except Exception as fallback_error:
503
+ # Even the fallback failed, return a simple response
504
+ error_msg = f"Main error: {e}\nFallback error: {fallback_error}"
505
+ print(f"Fallback agent also failed: {fallback_error}")
506
+ results["error"] = error_msg
507
+ results["answer"] = "I apologize, but I'm having technical difficulties processing your query. Please try again later."
508
+ return results