Commit d7ca098b authored by Kantz's avatar Kantz
Browse files

toolcalls in llm_clinet verschoben

parent 43e305bb
from ollama import chat
from app.deterministic_services import llm_client
def context_decision(needs_more_context: bool, reason: str) -> dict:
# Kannst auch einfach nur return {"needs_more_context": needs_more_context, "reason": reason}
return {"needs_more_context": bool(needs_more_context), "reason": str(reason)}
CLASSIFIER_SYSTEM = """
Du bist ein Klassifikator für didaktische Tutoring-Hinweise.
......@@ -41,23 +41,17 @@ def needs_more_context(history: str, context_sheet: str, model: str = "ministral
{"role": "user", "content": user_prompt},
]
resp = chat(
model=model,
resp, tool_outputs = llm_client.chat_with_tools(
messages=messages,
tools=[context_decision],
stream=False,
options={"temperature": 0},
use_ollama=True,
return_after_tools=True,
)
# 1) Ideal: strukturiertes Toolcall-Objekt
if resp.message.tool_calls:
call = resp.message.tool_calls[0]
if call.function.name != "context_decision":
return {"needs_more_context": True, "reason": f"Unexpected tool: {call.function.name}"}
args = call.function.arguments
# Tool ausführen (oder alternativ direkt args zurückgeben)
return context_decision(**args)
if tool_outputs:
result = tool_outputs[0].get("result")
if isinstance(result, dict) and "needs_more_context" in result:
return result
return {"needs_more_context": True, "reason": str(result)}
# 2) Fallback: falls Modell doch keinen Toolcall gemacht hat
return {"needs_more_context": True, "reason": resp.message.content or "No tool_call returned"}
return {"needs_more_context": True, "reason": llm_client.get_message_content(resp) or "No tool_call returned"}
import sympy as sp
from ollama import chat
from app.deterministic_services import llm_client
def sympy_solve(task: str, input: str, symbols: list[str] | None = None) -> str:
"""
......@@ -42,27 +44,9 @@ def solve_with_tools(user_text: str, model: str = "ministral-3") -> str:
{"role": "user", "content": user_text},
]
# 1) Modell darf Toolcalls erzeugen
resp = chat(model=model, messages=messages, tools=[sympy_solve], stream=False)
messages.append(resp.message)
# 2) Toolcalls ausführen und Ergebnisse zurückgeben
if resp.message.tool_calls:
for call in resp.message.tool_calls:
if call.function.name == "sympy_solve":
result = sympy_solve(**call.function.arguments)
else:
result = f"Unknown tool: {call.function.name}"
messages.append({
"role": "tool",
"tool_name": call.function.name,
"content": str(result),
})
# 3) Finalen Antwort-Call (Modell integriert Tool-Ergebnis)
final = chat(model=model, messages=messages, tools=[sympy_solve], stream=False)
return final.message.content
# Falls kein Toolcall: direkt zurück
return resp.message.content
result, _ = llm_client.chat_with_tools(
messages=messages,
tools=[sympy_solve],
use_ollama=True,
)
return llm_client.get_message_content(result)
import inspect
import json
from typing import Any, Callable
import ollama
from openai import OpenAI
......@@ -31,7 +33,7 @@ def _chat_openai(messages: list[dict]) -> dict:
def chat(
messages: list[dict],
tools: list[dict] | None = None,
tools: list[Callable[..., Any]] | None = None,
use_ollama: bool = False,
) -> dict:
if not tools and not use_ollama:
......@@ -61,7 +63,6 @@ def _extract_message(response) -> object:
return response.message
return {}
def get_message_content(result: dict | object) -> str:
message = result.get("message") if isinstance(result, dict) else result
if isinstance(message, dict):
......@@ -71,8 +72,53 @@ def get_message_content(result: dict | object) -> str:
return ""
def get_tool_calls(result: dict | object) -> list:
def chat_with_tools(
messages: list[dict],
tools: list[Callable[..., Any]],
use_ollama: bool = True,
return_after_tools: bool = False,
) -> tuple[dict, list[dict[str, Any]]]:
tool_map = {tool.__name__: tool for tool in tools}
result = chat(messages=messages, tools=tools, use_ollama=use_ollama)
tool_outputs = _apply_tool_calls(result, messages, tool_map)
if not tool_outputs or return_after_tools:
return result, tool_outputs
final_result = chat(messages=messages, tools=tools, use_ollama=use_ollama)
return final_result, tool_outputs
def _apply_tool_calls(
result: dict | object,
messages: list[dict],
tool_map: dict[str, Callable[..., Any]],
) -> list[dict[str, Any]]:
message = result.get("message") if isinstance(result, dict) else result
tool_calls = _extract_tool_calls(message)
outputs: list[dict[str, Any]] = []
if tool_calls:
messages.append(_message_to_dict(message))
for call in tool_calls:
name, arguments = _tool_call_name_args(call)
tool = tool_map.get(name)
if not tool:
output = f"Unknown tool: {name}"
else:
try:
output = tool(**arguments)
except Exception as exc: # pragma: no cover - defensive
output = f"Tool error: {exc}"
outputs.append({"name": name, "arguments": arguments, "result": output})
messages.append({"role": "tool", "tool_name": name, "content": str(output)})
return outputs
def _extract_tool_calls(message: object) -> list:
if isinstance(message, dict):
return message.get("tool_calls") or []
if hasattr(message, "tool_calls"):
......@@ -80,11 +126,42 @@ def get_tool_calls(result: dict | object) -> list:
return []
def normalize_tool_call(tool_call: object) -> dict:
if isinstance(tool_call, dict):
function = tool_call.get("function") or {}
return {"name": function.get("name"), "arguments": function.get("arguments")}
function = getattr(tool_call, "function", None)
if function:
return {"name": getattr(function, "name", None), "arguments": getattr(function, "arguments", None)}
return {"name": None, "arguments": None}
def _tool_call_name_args(call: object) -> tuple[str, dict[str, Any]]:
if isinstance(call, dict):
function = call.get("function") or {}
name = function.get("name") or ""
arguments = function.get("arguments")
else:
function = getattr(call, "function", None)
name = getattr(function, "name", "") if function else ""
arguments = getattr(function, "arguments", None) if function else None
return name, _parse_tool_arguments(arguments)
def _parse_tool_arguments(arguments: object) -> dict[str, Any]:
if isinstance(arguments, dict):
return arguments
if isinstance(arguments, str) and arguments.strip():
try:
parsed = json.loads(arguments)
if isinstance(parsed, dict):
return parsed
except json.JSONDecodeError:
return {}
return {}
def _message_to_dict(message: object) -> dict[str, Any]:
if isinstance(message, dict):
return message
role = getattr(message, "role", None)
content = getattr(message, "content", None)
tool_calls = getattr(message, "tool_calls", None)
payload: dict[str, Any] = {}
if role is not None:
payload["role"] = role
if content is not None:
payload["content"] = content
if tool_calls is not None:
payload["tool_calls"] = tool_calls
return payload
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment