YusufSimsek's picture
Add bookstore tool-calling assistant project
58f6928 verified
Raw
History Blame Contribute Delete
9.93 kB
import json
from typing import Any
from llm_client import LLMClient
from prompts import SYSTEM_PROMPT
from tool_schemas import get_tool_schemas
from tools import execute_tool
class BookstoreAgent:
"""
Kullanıcı mesajını modele gönderir, tool çağrılarını çalıştırır
ve veritabanı sonucuna dayalı nihai cevabı üretir.
"""
def __init__(
self,
*,
model_id: str | None = None,
max_tool_iterations: int = 6,
) -> None:
self.llm_client = LLMClient(model_id=model_id)
self.tool_schemas = get_tool_schemas(strict=False)
self.max_tool_iterations = max_tool_iterations
@staticmethod
def parse_tool_arguments(
raw_arguments: Any,
) -> dict[str, Any]:
"""
Farklı inference sağlayıcılarından gelebilecek tool
argümanlarını güvenli biçimde sözlüğe dönüştürür.
"""
if isinstance(raw_arguments, dict):
return raw_arguments
if isinstance(raw_arguments, str):
try:
parsed_arguments = json.loads(raw_arguments)
except json.JSONDecodeError as error:
raise ValueError(
"Model geçerli JSON tool parametresi üretmedi."
) from error
if not isinstance(parsed_arguments, dict):
raise ValueError(
"Tool parametrelerinin JSON nesnesi olması gerekiyor."
)
return parsed_arguments
raise ValueError(
"Tool parametreleri desteklenmeyen bir biçimde geldi."
)
@staticmethod
def prepare_history(
history: list[dict[str, str]] | None,
) -> list[dict[str, str]]:
"""
Önceki konuşmadan yalnızca user ve assistant mesajlarını alır.
"""
prepared_messages: list[dict[str, str]] = []
if not history:
return prepared_messages
# Çok uzun konuşmaların API isteğini şişirmemesi için
# yalnızca son 12 mesajı kullanıyoruz.
for message in history[-12:]:
role = message.get("role")
content = message.get("content", "")
if role not in {"user", "assistant"}:
continue
if not isinstance(content, str):
continue
prepared_messages.append(
{
"role": role,
"content": content,
}
)
return prepared_messages
def run(
self,
user_message: str,
history: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
"""
Tek bir kullanıcı isteğini uçtan uca işler.
Dönüş:
{
"success": bool,
"answer": str,
"tool_logs": [...]
}
"""
user_message = user_message.strip()
if not user_message:
return {
"success": False,
"answer": "Lütfen bir mesaj yazın.",
"tool_logs": [],
}
messages: list[Any] = [
{
"role": "system",
"content": SYSTEM_PROMPT,
}
]
messages.extend(self.prepare_history(history))
messages.append(
{
"role": "user",
"content": user_message,
}
)
tool_logs: list[dict[str, Any]] = []
# create_order yalnızca bu turda search_books tarafından
# döndürülmüş gerçek book_id değerlerini kullanabilir.
authorized_book_ids: set[int] = set()
# Model aynı kullanıcı mesajında yanlışlıkla ikinci kez
# sipariş oluşturmasın.
order_created_in_this_turn = False
for iteration in range(1, self.max_tool_iterations + 1):
response = self.llm_client.create_chat_completion(
messages,
tools=self.tool_schemas,
tool_choice="auto",
)
if not response.choices:
return {
"success": False,
"answer": "Model boş bir cevap döndürdü.",
"tool_logs": tool_logs,
}
response_message = response.choices[0].message
tool_calls = getattr(
response_message,
"tool_calls",
None,
)
# Tool çağrısı yoksa bu, kullanıcıya verilecek nihai cevaptır.
if not tool_calls:
answer = getattr(
response_message,
"content",
None,
)
if not answer:
answer = (
"İşlem tamamlandı ancak model metin cevabı "
"oluşturmadı."
)
return {
"success": True,
"answer": str(answer).strip(),
"tool_logs": tool_logs,
"iterations": iteration,
}
# Resmî HF akışında assistant tool-call mesajı,
# tool sonuçlarından önce konuşmaya eklenir.
messages.append(response_message)
for tool_call in tool_calls:
tool_name = tool_call.function.name
raw_arguments = tool_call.function.arguments
try:
arguments = self.parse_tool_arguments(
raw_arguments
)
except ValueError as error:
arguments = {}
tool_result = {
"success": False,
"error": str(error),
}
else:
# Modelin book_id uydurmasını kesin olarak engelle.
if tool_name == "create_order":
requested_book_id = arguments.get("book_id")
if order_created_in_this_turn:
tool_result = {
"success": False,
"error": (
"Bu kullanıcı isteği için zaten bir "
"sipariş oluşturuldu. İkinci sipariş "
"oluşturulmadı."
),
}
elif requested_book_id not in authorized_book_ids:
tool_result = {
"success": False,
"error": (
"create_order çağrısından önce "
"search_books kullanılmalı ve book_id "
"o tool sonucundan alınmalıdır."
),
"requested_book_id": requested_book_id,
}
else:
tool_result = execute_tool(
tool_name,
arguments,
)
else:
tool_result = execute_tool(
tool_name,
arguments,
)
# Başarılı arama sonucundaki gerçek kitap ID'lerini kaydet.
if (
tool_name == "search_books"
and tool_result.get("success") is True
):
for book in tool_result.get("books", []):
book_id = book.get("book_id")
if isinstance(book_id, int):
authorized_book_ids.add(book_id)
if (
tool_name == "create_order"
and tool_result.get("success") is True
):
order_created_in_this_turn = True
log_entry = {
"iteration": iteration,
"tool_call_id": tool_call.id,
"tool_name": tool_name,
"arguments": arguments,
"result": tool_result,
}
tool_logs.append(log_entry)
print("\n" + "=" * 80)
print(f"TOOL CALL: {tool_name}")
print("=" * 80)
print(
"Arguments:",
json.dumps(
arguments,
ensure_ascii=False,
indent=2,
),
)
print(
"Result:",
json.dumps(
tool_result,
ensure_ascii=False,
indent=2,
),
)
# Tool sonucunu yeniden modele gönderiyoruz.
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": tool_name,
"content": json.dumps(
tool_result,
ensure_ascii=False,
),
}
)
return {
"success": False,
"answer": (
"İşlem güvenlik sınırına ulaştığı için durduruldu. "
"Lütfen isteğinizi daha açık biçimde tekrar yazın."
),
"tool_logs": tool_logs,
"iterations": self.max_tool_iterations,
}