fix(agentic): DeepSeek llama tools de forma fiable (conector OpenAI + followup_mode)
Dos bugs encadenados impedían que el agente ejecutara tools (emitía los tool calls como texto sin ejecutarlos, y degradaba el contexto): 1. Conector: el OpenAIAdapter pasaba los mensajes en formato Anthropic (bloques tool_use/tool_result) que la API OpenAI de DeepSeek rechaza, y defaulteaba el modelo a "gpt-4o". Añade `_to_openai_messages()` (assistant.tool_use → tool_calls; user.tool_result → role:tool con tool_call_id) y `_blocks_text()`, y usa `settings.default_model_id`. Con esto DeepSeek devuelve tool_calls nativos vía https://api.deepseek.com (endpoint OpenAI), sin parsear texto y sin la degradación que sufría el endpoint Anthropic-compat. 2. followup_mode: `_classify_followup_mode` marcaba como "transform" cualquier PRIMER mensaje que contuviera un marker ("resumen", "estructura", "busca", "adapta"…), y `_get_allowed_tools` devuelve [] en modo transform → el agente se quedaba SIN tools. Un follow-up no tiene sentido sin turno anterior, así que ahora solo se clasifica si hay task_history/recent_messages. claude_adapter: parser DSML/DeepSeek para tool calls como texto (fallback del endpoint Anthropic-compat, ya no es la vía principal). Validado: el prompt de análisis de estilos ("Guarda un resumen…") ahora explora los módulos y escribe docs/project-styles.md vía save_project_styles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,10 +43,10 @@ class OpenAIAdapter(ModelAdapter):
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": config.model_id or "gpt-4o",
|
||||
"model": config.model_id or settings.default_model_id or "gpt-4o",
|
||||
"max_tokens": config.max_tokens,
|
||||
"temperature": config.temperature,
|
||||
"messages": messages,
|
||||
"messages": self._to_openai_messages(messages),
|
||||
"stream": True,
|
||||
"stream_options": {"include_usage": True},
|
||||
}
|
||||
@@ -145,10 +145,10 @@ class OpenAIAdapter(ModelAdapter):
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": config.model_id or "gpt-4o",
|
||||
"model": config.model_id or settings.default_model_id or "gpt-4o",
|
||||
"max_tokens": config.max_tokens,
|
||||
"temperature": config.temperature,
|
||||
"messages": messages,
|
||||
"messages": self._to_openai_messages(messages),
|
||||
}
|
||||
if tools:
|
||||
kwargs["tools"] = self._format_tools(tools)
|
||||
@@ -220,3 +220,77 @@ class OpenAIAdapter(ModelAdapter):
|
||||
}
|
||||
)
|
||||
return formatted
|
||||
|
||||
@staticmethod
|
||||
def _blocks_text(content: Any) -> str:
|
||||
"""Extrae texto plano de un content que puede ser str o lista de bloques."""
|
||||
if content is None:
|
||||
return ""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for b in content:
|
||||
if isinstance(b, dict):
|
||||
parts.append(b.get("text") or b.get("content") or "")
|
||||
else:
|
||||
parts.append(str(b))
|
||||
return "\n".join(p for p in parts if p)
|
||||
return str(content)
|
||||
|
||||
def _to_openai_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Convierte los mensajes del formato interno (Anthropic-style, con bloques
|
||||
`tool_use` / `tool_result`) al formato de la API OpenAI (`tool_calls` en el
|
||||
assistant, mensajes `role: tool` con `tool_call_id`). El contexto se construye
|
||||
en formato Anthropic, así que sin esto la API OpenAI de DeepSeek rechaza el
|
||||
body ('unknown variant tool_use')."""
|
||||
out: list[dict[str, Any]] = []
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
if role == "system":
|
||||
out.append({"role": "system", "content": content if isinstance(content, str) else self._blocks_text(content)})
|
||||
continue
|
||||
if not isinstance(content, list):
|
||||
out.append({"role": role, "content": content if isinstance(content, str) else str(content or "")})
|
||||
continue
|
||||
if role == "assistant":
|
||||
text_parts: list[str] = []
|
||||
tool_calls: list[dict[str, Any]] = []
|
||||
for b in content:
|
||||
if not isinstance(b, dict):
|
||||
continue
|
||||
t = b.get("type")
|
||||
if t == "text":
|
||||
text_parts.append(b.get("text", ""))
|
||||
elif t == "tool_use":
|
||||
tool_calls.append({
|
||||
"id": b.get("id", ""),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": b.get("name", ""),
|
||||
"arguments": json.dumps(b.get("input", {}), ensure_ascii=False),
|
||||
},
|
||||
})
|
||||
# thinking / otros bloques: se ignoran (OpenAI no los soporta)
|
||||
m: dict[str, Any] = {"role": "assistant", "content": ("\n".join(p for p in text_parts if p) or None)}
|
||||
if tool_calls:
|
||||
m["tool_calls"] = tool_calls
|
||||
out.append(m)
|
||||
else: # user (puede traer tool_result blocks)
|
||||
text_parts = []
|
||||
for b in content:
|
||||
if not isinstance(b, dict):
|
||||
continue
|
||||
t = b.get("type")
|
||||
if t == "tool_result":
|
||||
out.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": b.get("tool_use_id", ""),
|
||||
"content": self._blocks_text(b.get("content")),
|
||||
})
|
||||
elif t == "text":
|
||||
text_parts.append(b.get("text", ""))
|
||||
if text_parts:
|
||||
out.append({"role": "user", "content": "\n".join(text_parts)})
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user