import json import os from typing import List, Dict, Any, Tuple from openai import OpenAI import config import tools EXECUTION_LOGS: List[Dict[str, Any]] = [] import re SYSTEM_PROMPT = """Sen uzman bir Akademik Araştırma ve Makale Yönetim Asistanısın. Sistemde kullanabileceğin fonksiyonlar (tool call) tanımlanmıştır: 1. `search_arxiv_papers`: ArXiv platformunda makale arar. 2. `search_openalex`: OpenAlex platformunda atıf sayıları ile makale arar. 3. `save_paper_to_library`: Bulunan veya belirtilen bir makaleyi veritabanındaki kişisel kütüphaneye kaydeder. 4. `get_saved_library`: Kullanıcının kayıtlı makalelerini ve okuma durumlarını listeler. 5. `update_paper_status_or_note`: Kayıtlı makalelerin okuma durumunu ('unread', 'reading', 'completed') veya kişisel notlarını günceller. 6. `delete_paper`: Kullanıcının girdiği paper idsine veya başlığına sahip makaleyi veritabanından siler. 7. `clear_library`: Veritabanındaki tüm makaleleri kalıcı olarak siler ve kütüphaneyi temizler. ÖNEMLİ KURALLAR (MULTI-STEP TOOL CALL & HALÜSİNASYON ENGELLEME): - Kullanıcı bir makaleyi kütüphaneye eklemek/kaydetmek istediğinde (örn: 'LSTM makalesini kütüphaneye ekle'): 1. ÖNCE `search_arxiv_papers` veya `search_openalex` fonksiyonu ile makaleleri ara. 2. Dönen sonuçlar arasından ilgili makale(ler) için `save_paper_to_library` fonksiyonunu çağırarak kütüphaneye kaydet. 3. Kayıt gerçekleştikten sonra kullanıcıya kaydedilen makalelerin başlık ve detaylarını özetle. - KESİNLİKLE `` veya iç düşünce bloklarını yanıtlarına koyma. Doğrudan net yanıt ver. - KESİNLİKLE veritabanında veya API aramasında olmayan uydurma bir makale, yazar veya istatistik SÖYLEME. - Tüm yanıtların SADECE ve SADECE en son fonksiyonlardan (tool call) dönen gerçek veriye dayanmalıdır. - EĞER `get_saved_library` çağrıldığında dönen kütüphane boşsa (`library: []` veya `saved_count: 0`), KESİNLİKLE sohbet geçmişindeki eski silinmiş makaleleri kütüphanedeymiş gibi LISTELEME! Kütüphanenin boş olduğunu açıkça söyle. - Türkçe, kibar ve akademik bir dil kullan. """ def log_execution(step_type: str, name: str, input_data: Any, output_data: Any): """Arka planda gerçekleşen Tool-Call işlemlerini loglar.""" log_entry = { "timestamp": json.loads(json.dumps(str(os.times()))), "type": step_type, "name": name, "input": input_data, "output": output_data } EXECUTION_LOGS.append(log_entry) def get_client_and_model(custom_api_key: str = "", custom_provider: str = "", custom_model: str = "") -> Tuple[OpenAI, str]: """Seçilen konfigürasyona göre OpenAI-uyumlu client ve model ismini döndürür.""" groq_key = custom_api_key or config.GROQ_API_KEY openrouter_key = custom_api_key or config.OPENROUTER_API_KEY openai_key = custom_api_key or config.OPENAI_API_KEY if custom_provider == "Groq" or (groq_key and not custom_provider): client = OpenAI( base_url="https://api.groq.com/openai/v1", api_key=groq_key or "dummy_key" ) model = custom_model or "llama-3.3-70b-versatile" elif custom_provider == "OpenRouter" or (openrouter_key and not custom_provider): client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=openrouter_key or "dummy_key" ) model = custom_model or "meta-llama/llama-3.1-8b-instruct" elif custom_provider == "Ollama (Remote/Local)": client = OpenAI( base_url=config.OLLAMA_BASE_URL, api_key="ollama" ) model = custom_model or "llama3.2" elif custom_provider == "OpenAI" or openai_key: client = OpenAI( api_key=openai_key or "dummy_key" ) model = custom_model or "gpt-4o-mini" else: client = OpenAI( base_url="https://api.groq.com/openai/v1" if groq_key else config.OLLAMA_BASE_URL, api_key=groq_key or "ollama" ) model = custom_model or config.DEFAULT_MODEL return client, model def process_user_query( user_message: str, chat_history: List[Dict[str, str]], api_key: str = "", provider: str = config.DEFAULT_PROVIDER, model_name: str = config.DEFAULT_MODEL ) -> Tuple[str, List[Dict[str, str]]]: """ Kullanıcı mesajını alır, LLM veya Intent Engine ile tool call süreçlerini yürütür. Çok adımlı (multi-step) tool çağrılarını desteklemek için döngü içerir. """ messages = [{"role": "system", "content": SYSTEM_PROMPT}] for msg in chat_history: messages.append(msg) messages.append({"role": "user", "content": user_message}) try: client, target_model = get_client_and_model(api_key, provider, model_name) max_iterations = 5 iteration = 0 final_content = "" while iteration < max_iterations: iteration += 1 response = client.chat.completions.create( model=target_model, messages=messages, tools=tools.TOOLS_SCHEMA, tool_choice="auto" ) response_message = response.choices[0].message content_text = response_message.content or "" if response_message.tool_calls: messages.append(response_message) for tool_call in response_message.tool_calls: function_name = tool_call.function.name try: raw_args = tool_call.function.arguments function_args = json.loads(raw_args) if raw_args else {} except Exception: function_args = {} if not isinstance(function_args, dict): function_args = {} else: function_args = {k: v for k, v in function_args.items() if v is not None} log_execution("TOOL_CALL_TRIGGERED", function_name, function_args, None) tool_response = tools.execute_tool(function_name, function_args) log_execution("TOOL_CALL_RESULT", function_name, function_args, tool_response) messages.append({ "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": json.dumps(tool_response, ensure_ascii=False) }) # Multi-step: Döngüye devam et ki LLM sonuçları görüp ikinci bir tool (örn: save_paper) çağırabilsin continue # 2. Metin Tabanı veya Taglerini Yakalama elif "" in content_text or "" in content_text: text_tool_matches = re.findall( r'<(?:function|tool_call)>([^<]+)\s*(\{.*?\})', content_text, flags=re.DOTALL ) if text_tool_matches: messages.append({"role": "assistant", "content": content_text}) executed_results = [] for func_name, raw_json in text_tool_matches: func_name = func_name.strip() try: func_args = json.loads(raw_json.strip()) except Exception: func_args = {} if isinstance(func_args, dict): func_args = {k: v for k, v in func_args.items() if v is not None} else: func_args = {} log_execution("TOOL_CALL_TRIGGERED (TEXT_TAG)", func_name, func_args, None) t_res = tools.execute_tool(func_name, func_args) log_execution("TOOL_CALL_RESULT (TEXT_TAG)", func_name, func_args, t_res) executed_results.append(f"Tool '{func_name}' sonucu: {json.dumps(t_res, ensure_ascii=False)}") messages.append({ "role": "user", "content": "Çalıştırılan fonksiyon sonuçları:\n" + "\n".join(executed_results) }) continue else: final_content = content_text break else: final_content = content_text break if not final_content and len(messages) > 0: last_msg = messages[-1] if isinstance(last_msg, dict) and last_msg.get("role") == "tool": final_content = "İşlem başarıyla tamamlandı." else: final_content = content_text or "İşlem tamamlandı." # Temizlik: ve etiket artıkları if final_content: final_content = re.sub(r'.*?', '', final_content, flags=re.DOTALL) final_content = re.sub(r'<(?:function|tool_call)>.*?\s*\{.*?\}', '', final_content, flags=re.DOTALL) final_content = final_content.strip() return final_content or "İşlem tamamlandı.", EXECUTION_LOGS except Exception as e: error_msg = f"API Çağrı Hatası ({str(e)})." log_execution("ERROR", "API_FAIL", {"error": str(error_msg)}, None) return f"⚠️ {error_msg}", EXECUTION_LOGS