Álvaro Valenzuela Valdes commited on
Commit
5642e68
·
1 Parent(s): ef0e7f7

Fix async LLM calls and agents orchestration to resolve chat connectivity error

Browse files
backend/app/services/agents.py CHANGED
@@ -18,7 +18,7 @@ async def legal_agent_task(tender: Tender, company: CompanyProfile, document_tex
18
  f"EXTRACTED TEXT: {document_text[:5000]}\n"
19
  f"TASK: Identify 3 legal gaps. Analyze if the timeline is 'Express' (suspiciously short) for the tender type {tender.type}. Verify if company documents meet common requirements for this tender level."
20
  )
21
- return await asyncio.to_thread(call_gemini_with_model, prompt, model)
22
 
23
  async def technical_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None) -> str:
24
  items_summary = ", ".join([f"{i.name} ({i.quantity} {i.unit})" for i in (tender.items or [])[:10]])
@@ -31,7 +31,7 @@ async def technical_agent_task(tender: Tender, company: CompanyProfile, document
31
  f"EXTRACTED TEXT: {document_text[:5000]}\n"
32
  f"TASK: Analyze specific LINE ITEMS against company capabilities. Identify 3 technical challenges. Is this a generic 'buy' or a complex project?"
33
  )
34
- return await asyncio.to_thread(call_gemini_with_model, prompt, model)
35
 
36
  async def strategy_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None) -> str:
37
  prompt = (
@@ -43,7 +43,7 @@ async def strategy_agent_task(tender: Tender, company: CompanyProfile, document_
43
  f"COMPANY: {company.name}\n"
44
  f"TASK: Identify 3 strategic risks. Pay special attention to CURRENCY RISK if not CLP ({tender.currency}). Suggest a 'Win Strategy' based on the tender type {tender.type}."
45
  )
46
- return await asyncio.to_thread(call_gemini_with_model, prompt, model)
47
 
48
  async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
49
  audit_log = ["🚀 Iniciando mesa de expertos agéntica..."]
@@ -87,7 +87,7 @@ async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, doc
87
  f"Responde ÚNICAMENTE con el JSON."
88
  )
89
 
90
- final_output = await asyncio.to_thread(call_gemini, synthesis_prompt)
91
  parse_result = _parse_gemini_response(final_output)
92
 
93
  if parse_result:
@@ -105,4 +105,4 @@ async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, doc
105
 
106
  # Fallback
107
  from app.services.llm import generate_analysis
108
- return generate_analysis(tender, company_profile, doc_text, models)
 
18
  f"EXTRACTED TEXT: {document_text[:5000]}\n"
19
  f"TASK: Identify 3 legal gaps. Analyze if the timeline is 'Express' (suspiciously short) for the tender type {tender.type}. Verify if company documents meet common requirements for this tender level."
20
  )
21
+ return await call_gemini_with_model(prompt, model)
22
 
23
  async def technical_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None) -> str:
24
  items_summary = ", ".join([f"{i.name} ({i.quantity} {i.unit})" for i in (tender.items or [])[:10]])
 
31
  f"EXTRACTED TEXT: {document_text[:5000]}\n"
32
  f"TASK: Analyze specific LINE ITEMS against company capabilities. Identify 3 technical challenges. Is this a generic 'buy' or a complex project?"
33
  )
34
+ return await call_gemini_with_model(prompt, model)
35
 
36
  async def strategy_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "", model: str | None = None) -> str:
37
  prompt = (
 
43
  f"COMPANY: {company.name}\n"
44
  f"TASK: Identify 3 strategic risks. Pay special attention to CURRENCY RISK if not CLP ({tender.currency}). Suggest a 'Win Strategy' based on the tender type {tender.type}."
45
  )
46
+ return await call_gemini_with_model(prompt, model)
47
 
48
  async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
49
  audit_log = ["🚀 Iniciando mesa de expertos agéntica..."]
 
87
  f"Responde ÚNICAMENTE con el JSON."
88
  )
89
 
90
+ final_output = await call_gemini(synthesis_prompt)
91
  parse_result = _parse_gemini_response(final_output)
92
 
93
  if parse_result:
 
105
 
106
  # Fallback
107
  from app.services.llm import generate_analysis
108
+ return await generate_analysis(tender, company_profile, doc_text, models)
backend/app/services/llm.py CHANGED
@@ -27,26 +27,27 @@ def get_gemini_model():
27
  }
28
  )
29
 
30
- def call_gemini(prompt: str) -> str:
31
  if not settings.gemini_api_key:
32
  # Fallback to Featherless if Gemini not available
33
- return call_featherless(prompt, "meta-llama/Llama-3.3-70B-Instruct")
34
 
35
  try:
36
  model = get_gemini_model()
37
- response = model.generate_content(prompt)
 
38
  return response.text
39
  except Exception as e:
40
  print(f"Error calling Gemini: {e}, trying Featherless fallback...")
41
- return call_featherless(prompt, "meta-llama/Llama-3.3-70B-Instruct")
42
 
43
- def call_featherless(prompt: str, model: str = "deepseek-ai/DeepSeek-V3.2") -> str:
44
  if not settings.featherless_api_key:
45
  return ""
46
 
47
  try:
48
- with httpx.Client(timeout=60.0) as client:
49
- response = client.post(
50
  "https://api.featherless.ai/v1/chat/completions",
51
  headers={
52
  "Authorization": f"Bearer {settings.featherless_api_key}",
@@ -65,7 +66,7 @@ def call_featherless(prompt: str, model: str = "deepseek-ai/DeepSeek-V3.2") -> s
65
  print(f"Error calling Featherless ({model}): {e}")
66
  return ""
67
 
68
- def call_gemini_with_model(prompt: str, model_name: str | None = None) -> str:
69
  # Default model mapping
70
  model_map = {
71
  "Gemini 2.5 Flash": "gemini",
@@ -79,9 +80,9 @@ def call_gemini_with_model(prompt: str, model_name: str | None = None) -> str:
79
  model_id = model_map.get(model_name, "gemini")
80
 
81
  if model_id == "gemini":
82
- return call_gemini(prompt)
83
  else:
84
- return call_featherless(prompt, model=model_id)
85
 
86
  def _normalize_gemini_output(output: str) -> str:
87
  if not output:
@@ -158,7 +159,7 @@ def generate_mock_analysis(tender: Tender, company: CompanyProfile) -> AnalysisR
158
  audit_log=["Iniciando análisis de respaldo...", "Generando datos mock."]
159
  )
160
 
161
- def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
162
  # Default model mapping
163
  model_map = {
164
  "Gemini 2.5 Flash": "gemini",
@@ -202,13 +203,13 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
202
 
203
  res = ""
204
  if model_id == "gemini":
205
- res = call_gemini(agent_prompt)
206
  # Failover to Featherless if Gemini fails (e.g. 429)
207
  if not res and settings.featherless_api_key:
208
  audit_messages.append(f"🔄 Gemini failed/rate-limited. Switching to DeepSeek for {agent_id.upper()}...")
209
- res = call_featherless(agent_prompt, model="deepseek-ai/DeepSeek-V3.2")
210
  else:
211
- res = call_featherless(agent_prompt, model=model_id)
212
 
213
  agent_outputs[agent_id] = res or "Análisis no disponible por error de API."
214
 
@@ -233,9 +234,9 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
233
  RESPONDE SOLO EL JSON.
234
  """
235
 
236
- final_json = call_gemini(synthesis_prompt)
237
  if not final_json and settings.featherless_api_key:
238
- final_json = call_featherless(synthesis_prompt, model="deepseek-ai/DeepSeek-V3.2")
239
 
240
  parse_result = _parse_gemini_response(final_json)
241
 
@@ -244,7 +245,7 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
244
  # FORCE PROPOSAL GENERATION
245
  if not parse_result.get("proposal_draft") or len(parse_result["proposal_draft"]) < 100:
246
  audit_messages.append("📝 Generating specialized proposal draft...")
247
- parse_result["proposal_draft"] = generate_proposal_draft(parse_result, company)
248
 
249
  result = AnalysisResult(**parse_result)
250
  result.audit_log = audit_messages + (result.audit_log or [])
@@ -257,7 +258,7 @@ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: st
257
  analysis.audit_log = audit_messages + ["⚠️ Synthesis failed, using emergency fallback."]
258
  return analysis
259
 
260
- def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
261
  prompt = f"""
262
  Como experto redactor de propuestas de licitación, genera un borrador profesional (en Markdown) basado en este análisis técnico:
263
  {analysis.get('executive_summary', 'Analizar bases adjuntas.')}
@@ -274,13 +275,13 @@ def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
274
 
275
  # Use DeepSeek for high-quality drafting if available
276
  if settings.featherless_api_key:
277
- draft = call_featherless(prompt, model="deepseek-ai/DeepSeek-V3.2")
278
  if draft: return draft
279
 
280
  # Fallback to Gemini
281
- return call_gemini(prompt) or "Error al generar el borrador de la propuesta."
282
 
283
- def generate_synthetic_tenders(keyword: str) -> list[Tender]:
284
  """
285
  Generates realistic-looking synthetic tenders using an LLM.
286
  Used as a fallback when the real scraper is blocked.
@@ -302,9 +303,9 @@ def generate_synthetic_tenders(keyword: str) -> list[Tender]:
302
  Responde ÚNICAMENTE un JSON con una lista de objetos bajo la llave 'tenders'.
303
  """
304
 
305
- res = call_gemini(prompt)
306
  if not res and settings.featherless_api_key:
307
- res = call_featherless(prompt)
308
 
309
  data = _parse_gemini_response(res)
310
  results = []
 
27
  }
28
  )
29
 
30
+ async def call_gemini(prompt: str) -> str:
31
  if not settings.gemini_api_key:
32
  # Fallback to Featherless if Gemini not available
33
+ return await call_featherless(prompt, "meta-llama/Llama-3.3-70B-Instruct")
34
 
35
  try:
36
  model = get_gemini_model()
37
+ # Use run_in_executor if the library is blocking, or use async generate_content if supported
38
+ response = await model.generate_content_async(prompt)
39
  return response.text
40
  except Exception as e:
41
  print(f"Error calling Gemini: {e}, trying Featherless fallback...")
42
+ return await call_featherless(prompt, "meta-llama/Llama-3.3-70B-Instruct")
43
 
44
+ async def call_featherless(prompt: str, model: str = "deepseek-ai/DeepSeek-V3.2") -> str:
45
  if not settings.featherless_api_key:
46
  return ""
47
 
48
  try:
49
+ async with httpx.AsyncClient(timeout=60.0) as client:
50
+ response = await client.post(
51
  "https://api.featherless.ai/v1/chat/completions",
52
  headers={
53
  "Authorization": f"Bearer {settings.featherless_api_key}",
 
66
  print(f"Error calling Featherless ({model}): {e}")
67
  return ""
68
 
69
+ async def call_gemini_with_model(prompt: str, model_name: str | None = None) -> str:
70
  # Default model mapping
71
  model_map = {
72
  "Gemini 2.5 Flash": "gemini",
 
80
  model_id = model_map.get(model_name, "gemini")
81
 
82
  if model_id == "gemini":
83
+ return await call_gemini(prompt)
84
  else:
85
+ return await call_featherless(prompt, model=model_id)
86
 
87
  def _normalize_gemini_output(output: str) -> str:
88
  if not output:
 
159
  audit_log=["Iniciando análisis de respaldo...", "Generando datos mock."]
160
  )
161
 
162
+ async def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None, models: dict | None = None) -> AnalysisResult:
163
  # Default model mapping
164
  model_map = {
165
  "Gemini 2.5 Flash": "gemini",
 
203
 
204
  res = ""
205
  if model_id == "gemini":
206
+ res = await call_gemini(agent_prompt)
207
  # Failover to Featherless if Gemini fails (e.g. 429)
208
  if not res and settings.featherless_api_key:
209
  audit_messages.append(f"🔄 Gemini failed/rate-limited. Switching to DeepSeek for {agent_id.upper()}...")
210
+ res = await call_featherless(agent_prompt, model="deepseek-ai/DeepSeek-V3.2")
211
  else:
212
+ res = await call_featherless(agent_prompt, model=model_id)
213
 
214
  agent_outputs[agent_id] = res or "Análisis no disponible por error de API."
215
 
 
234
  RESPONDE SOLO EL JSON.
235
  """
236
 
237
+ final_json = await call_gemini(synthesis_prompt)
238
  if not final_json and settings.featherless_api_key:
239
+ final_json = await call_featherless(synthesis_prompt, model="deepseek-ai/DeepSeek-V3.2")
240
 
241
  parse_result = _parse_gemini_response(final_json)
242
 
 
245
  # FORCE PROPOSAL GENERATION
246
  if not parse_result.get("proposal_draft") or len(parse_result["proposal_draft"]) < 100:
247
  audit_messages.append("📝 Generating specialized proposal draft...")
248
+ parse_result["proposal_draft"] = await generate_proposal_draft(parse_result, company)
249
 
250
  result = AnalysisResult(**parse_result)
251
  result.audit_log = audit_messages + (result.audit_log or [])
 
258
  analysis.audit_log = audit_messages + ["⚠️ Synthesis failed, using emergency fallback."]
259
  return analysis
260
 
261
+ async def generate_proposal_draft(analysis: dict, company: CompanyProfile) -> str:
262
  prompt = f"""
263
  Como experto redactor de propuestas de licitación, genera un borrador profesional (en Markdown) basado en este análisis técnico:
264
  {analysis.get('executive_summary', 'Analizar bases adjuntas.')}
 
275
 
276
  # Use DeepSeek for high-quality drafting if available
277
  if settings.featherless_api_key:
278
+ draft = await call_featherless(prompt, model="deepseek-ai/DeepSeek-V3.2")
279
  if draft: return draft
280
 
281
  # Fallback to Gemini
282
+ return await call_gemini(prompt) or "Error al generar el borrador de la propuesta."
283
 
284
+ async def generate_synthetic_tenders(keyword: str) -> list[Tender]:
285
  """
286
  Generates realistic-looking synthetic tenders using an LLM.
287
  Used as a fallback when the real scraper is blocked.
 
303
  Responde ÚNICAMENTE un JSON con una lista de objetos bajo la llave 'tenders'.
304
  """
305
 
306
+ res = await call_gemini(prompt)
307
  if not res and settings.featherless_api_key:
308
+ res = await call_featherless(prompt)
309
 
310
  data = _parse_gemini_response(res)
311
  results = []
backend/app/services/scraper.py CHANGED
@@ -42,14 +42,14 @@ async def scrape_compra_agil(keywords: str) -> List[Tender]:
42
 
43
  if response.status_code != 200:
44
  print(f"⚠️ API blocked (Status {response.status_code}). Activating Synthetic Fallback...")
45
- return generate_synthetic_tenders(keywords)
46
 
47
  raw_data = response.json()
48
  items = raw_data.get("data", [])
49
 
50
  if not items:
51
  print(f"ℹ️ No real results found for '{keywords}'. Using Synthetic Intelligence to find potential leads.")
52
- return generate_synthetic_tenders(keywords)
53
 
54
  tenders = []
55
  for item in items:
@@ -85,6 +85,6 @@ async def scrape_compra_agil(keywords: str) -> List[Tender]:
85
  except Exception as e:
86
  print(f"❌ Scraper failure: {e}. Activating emergency fallback.")
87
  try:
88
- return generate_synthetic_tenders(keywords)
89
  except:
90
  return []
 
42
 
43
  if response.status_code != 200:
44
  print(f"⚠️ API blocked (Status {response.status_code}). Activating Synthetic Fallback...")
45
+ return await generate_synthetic_tenders(keywords)
46
 
47
  raw_data = response.json()
48
  items = raw_data.get("data", [])
49
 
50
  if not items:
51
  print(f"ℹ️ No real results found for '{keywords}'. Using Synthetic Intelligence to find potential leads.")
52
+ return await generate_synthetic_tenders(keywords)
53
 
54
  tenders = []
55
  for item in items:
 
85
  except Exception as e:
86
  print(f"❌ Scraper failure: {e}. Activating emergency fallback.")
87
  try:
88
+ return await generate_synthetic_tenders(keywords)
89
  except:
90
  return []