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, }