victor34593993 commited on
Commit
ddefbcd
·
verified ·
1 Parent(s): 187966e

reliable tools (gpt-oss-120b) + resilient orchestrator

Browse files
Files changed (3) hide show
  1. app/config.py +3 -1
  2. app/orchestrator.py +20 -4
  3. tests/test_orchestrator.py +36 -1
app/config.py CHANGED
@@ -18,7 +18,9 @@ class Settings(BaseSettings):
18
  # LLM: Groq (primary)
19
  groq_api_key: str = ""
20
  groq_base_url: str = "https://api.groq.com/openai/v1"
21
- model_large: str = "llama-3.3-70b-versatile"
 
 
22
  model_small: str = "llama-3.1-8b-instant"
23
 
24
  # LLM: Cloudflare Workers AI (failover)
 
18
  # LLM: Groq (primary)
19
  groq_api_key: str = ""
20
  groq_base_url: str = "https://api.groq.com/openai/v1"
21
+ # gpt-oss-120b: stable on Groq + reliable tool calling (Llama 3.3 70B
22
+ # intermittently returns tool_use_failed on Groq).
23
+ model_large: str = "openai/gpt-oss-120b"
24
  model_small: str = "llama-3.1-8b-instant"
25
 
26
  # LLM: Cloudflare Workers AI (failover)
app/orchestrator.py CHANGED
@@ -7,7 +7,7 @@ from typing import Any
7
 
8
  from sqlalchemy import select
9
 
10
- from app.llm.base import ToolCall
11
  from app.models import ChatMessage
12
  from app.prompts import build_system_prompt
13
  from app.schemas import ChatResponse
@@ -61,8 +61,24 @@ async def run_turn(
61
  used_tools: list[str] = []
62
  reply = FALLBACK_REPLY
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  for _ in range(MAX_TOOL_ITERS):
65
- result = await router.chat(messages=messages, tools=registry.specs(), tier="large")
 
 
 
66
  if result.tool_calls:
67
  messages.append(
68
  {
@@ -86,8 +102,8 @@ async def run_turn(
86
  break
87
  else:
88
  # Tool budget exhausted: force a final answer with no tools.
89
- final = await router.chat(messages=messages, tools=[], tier="large")
90
- reply = final.content or FALLBACK_REPLY
91
 
92
  ctx.db.add(ChatMessage(session_id=ctx.session.id, role="assistant", content=reply))
93
  await ctx.db.flush()
 
7
 
8
  from sqlalchemy import select
9
 
10
+ from app.llm.base import ProviderError, ToolCall
11
  from app.models import ChatMessage
12
  from app.prompts import build_system_prompt
13
  from app.schemas import ChatResponse
 
61
  used_tools: list[str] = []
62
  reply = FALLBACK_REPLY
63
 
64
+ async def _chat(tools):
65
+ """Resilient call: if tool-calling fails (e.g. Groq tool_use_failed) or a
66
+ provider errors, degrade to a no-tools answer so we never 500."""
67
+ try:
68
+ return await router.chat(messages=messages, tools=tools, tier="large")
69
+ except ProviderError:
70
+ if not tools:
71
+ return None
72
+ try:
73
+ return await router.chat(messages=messages, tools=[], tier="large")
74
+ except ProviderError:
75
+ return None
76
+
77
  for _ in range(MAX_TOOL_ITERS):
78
+ result = await _chat(registry.specs())
79
+ if result is None:
80
+ reply = FALLBACK_REPLY
81
+ break
82
  if result.tool_calls:
83
  messages.append(
84
  {
 
102
  break
103
  else:
104
  # Tool budget exhausted: force a final answer with no tools.
105
+ final = await _chat([])
106
+ reply = (final.content if final else None) or FALLBACK_REPLY
107
 
108
  ctx.db.add(ChatMessage(session_id=ctx.session.id, role="assistant", content=reply))
109
  await ctx.db.flush()
tests/test_orchestrator.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
 
3
  import pytest
4
 
5
- from app.llm.base import ChatResult, ToolCall
6
  from app.models import ChatMessage, ChatSession
7
  from app.orchestrator import run_turn
8
  from app.tools.registry import ToolContext
@@ -87,6 +87,41 @@ async def test_order_turn_unverified_asks_for_credentials(db_session):
87
  assert resp.used_tools == ["lookup_order"]
88
 
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  async def test_tool_budget_exhausted_forces_final_answer(db_session):
91
  # Always returns tool_calls; orchestrator must stop and force a final answer.
92
  loop_result = ChatResult(
 
2
 
3
  import pytest
4
 
5
+ from app.llm.base import ChatResult, ProviderError, ToolCall
6
  from app.models import ChatMessage, ChatSession
7
  from app.orchestrator import run_turn
8
  from app.tools.registry import ToolContext
 
87
  assert resp.used_tools == ["lookup_order"]
88
 
89
 
90
+ class _ToolFailRouter:
91
+ """Fails (ProviderError) whenever tools are sent; answers when no tools."""
92
+
93
+ def __init__(self, reply="Respuesta sin herramientas."):
94
+ self.reply = reply
95
+ self.calls_with_tools = 0
96
+
97
+ async def chat(self, messages, tools, tier="large", temperature=None):
98
+ if tools:
99
+ self.calls_with_tools += 1
100
+ raise ProviderError("tool_use_failed")
101
+ return ChatResult(content=self.reply, tool_calls=[], finish_reason="stop")
102
+
103
+
104
+ class _AlwaysFailRouter:
105
+ async def chat(self, messages, tools, tier="large", temperature=None):
106
+ raise ProviderError("down")
107
+
108
+
109
+ async def test_degrades_to_no_tools_when_tool_calling_fails(db_session):
110
+ router = _ToolFailRouter()
111
+ ctx = ToolContext(db=db_session, session=await _session(db_session))
112
+ resp = await run_turn(router, ctx, "¿qué vendéis?")
113
+ assert resp.reply == "Respuesta sin herramientas."
114
+ assert router.calls_with_tools >= 1 # it tried tools first, then degraded
115
+
116
+
117
+ async def test_total_provider_failure_returns_fallback_not_500(db_session):
118
+ from app.orchestrator import FALLBACK_REPLY
119
+
120
+ ctx = ToolContext(db=db_session, session=await _session(db_session))
121
+ resp = await run_turn(_AlwaysFailRouter(), ctx, "hola")
122
+ assert resp.reply == FALLBACK_REPLY
123
+
124
+
125
  async def test_tool_budget_exhausted_forces_final_answer(db_session):
126
  # Always returns tool_calls; orchestrator must stop and force a final answer.
127
  loop_result = ChatResult(