1 line
41 KiB
JSON
1 line
41 KiB
JSON
{"repo": "blackfireoff/finbuddy", "files": {"back/app/ai.py": {"d9e1fb879943e49d98a747a77955a6107949ff43": {"t": 1785745988.6333647, "n": 4974, "h": "c834194efce3542d", "c": "import json\nfrom typing import Dict, Tuple\nimport os\nfrom fastapi import HTTPException\nfrom jinja2 import Template\nfrom json_repair import repair_json\n\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(\n status_code=400, detail=\"ai.api_key is required for non-ollama providers\"\n )\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n \"asi1\": {\n \"base_url\": \"https://api.asi1.ai/v1\",\n \"models\": [\"asi1-mini\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", default=\"http://localhost:11434\"),\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"openai client not installed: {str(e)}\"\n )\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": user_prompt}],\n )\n msg = None\n try:\n msg = resp.choices[0].message.content if resp and resp.choices else None\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = (\n msg\n if isinstance(msg, str)\n else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n )\n return text, model\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\"\n )\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n used_model = \"unknown\"\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump(),\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(\n f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\",\n flush=True,\n )\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores.model_dump() # ensure dict for Pydantic\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )\n"}, "9797155bbdb60999673e0c57630413ed8680f608": {"t": 1785758571.332112, "n": 4974, "h": "c834194efce3542d", "c": "import json\nfrom typing import Dict, Tuple\nimport os\nfrom fastapi import HTTPException\nfrom jinja2 import Template\nfrom json_repair import repair_json\n\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(\n status_code=400, detail=\"ai.api_key is required for non-ollama providers\"\n )\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n \"asi1\": {\n \"base_url\": \"https://api.asi1.ai/v1\",\n \"models\": [\"asi1-mini\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", default=\"http://localhost:11434\"),\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"openai client not installed: {str(e)}\"\n )\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": user_prompt}],\n )\n msg = None\n try:\n msg = resp.choices[0].message.content if resp and resp.choices else None\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = (\n msg\n if isinstance(msg, str)\n else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n )\n return text, model\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\"\n )\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n used_model = \"unknown\"\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump(),\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(\n f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\",\n flush=True,\n )\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores.model_dump() # ensure dict for Pydantic\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )\n"}, "08703811d4af064470c9239e3f0eb422884fb09a": {"t": 1785758572.7533653, "n": 4930, "h": "69c4ef23420efab2", "c": "import json\nfrom typing import Dict, Tuple\n\nfrom fastapi import HTTPException\nfrom jinja2 import Template\nfrom json_repair import repair_json\n\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(\n status_code=400, detail=\"ai.api_key is required for non-ollama providers\"\n )\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n \"asi1\": {\n \"base_url\": \"https://api.asi1.ai/v1\",\n \"models\": [\"asi1-mini\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": \"http://localhost:11434/v1\",\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"openai client not installed: {str(e)}\"\n )\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": user_prompt}],\n )\n msg = None\n try:\n msg = resp.choices[0].message.content if resp and resp.choices else None\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = (\n msg\n if isinstance(msg, str)\n else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n )\n return text, model\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\"\n )\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n used_model = \"unknown\"\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump(),\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(\n f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\",\n flush=True,\n )\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores.model_dump() # ensure dict for Pydantic\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )\n"}, "91dd96d8fdd4adf56c96f515c893dc495d6f2b05": {"t": 1785758574.0917826, "n": 4814, "h": "4a2aad6694ba8dd6", "c": "import json\nfrom typing import Dict, Tuple\n\nfrom fastapi import HTTPException\nfrom jinja2 import Template\nfrom json_repair import repair_json\n\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(\n status_code=400, detail=\"ai.api_key is required for non-ollama providers\"\n )\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": \"http://localhost:11434/v1\",\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"openai client not installed: {str(e)}\"\n )\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": user_prompt}],\n )\n msg = None\n try:\n msg = resp.choices[0].message.content if resp and resp.choices else None\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = (\n msg\n if isinstance(msg, str)\n else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n )\n return text, model\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\"\n )\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n used_model = \"unknown\"\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump(),\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(\n f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\",\n flush=True,\n )\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores.model_dump() # ensure dict for Pydantic\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )\n"}, "c2b8c889e9250b5f6e9df8e11db6a182bfb398b6": {"t": 1785758575.7955198, "n": 4761, "h": "bcd419caabcb3636", "c": "import json\nfrom typing import Any, Dict, Tuple\n\nfrom fastapi import FastAPI, HTTPException\nfrom jinja2 import Template\nfrom json_repair import repair_json\n\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(\n status_code=400, detail=\"ai.api_key is required for non-ollama providers\"\n )\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": \"http://localhost:11434/v1\",\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"openai client not installed: {str(e)}\"\n )\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": user_prompt}],\n )\n msg = None\n try:\n msg = resp.choices[0].message.content if resp and resp.choices else None\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = (\n msg\n if isinstance(msg, str)\n else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n )\n return text, model\n except Exception as e:\n raise HTTPException(\n status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\"\n )\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump(),\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(\n f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\",\n flush=True,\n )\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )\n\n"}, "4e3e37030055dc6bd5a575ea42117e0090afc4d6": {"t": 1785758577.2748375, "n": 4639, "h": "8da3e42327665939", "c": "import json\nfrom typing import Any, Dict, Tuple\nfrom fastapi import FastAPI, HTTPException\nfrom jinja2 import Template\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\nfrom json_repair import repair_json\n\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(status_code=400, detail=\"ai.api_key is required for non-ollama providers\")\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": \"http://localhost:11434/v1\",\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"openai client not installed: {str(e)}\")\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[\n {\"role\": \"user\", \"content\": user_prompt}\n ],\n )\n msg = None\n try:\n msg = (resp.choices[0].message.content if resp and resp.choices else None)\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = msg if isinstance(msg, str) else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n return text, model\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\")\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump()\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\", flush=True)\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )"}, "e9383af60ae5becb3c6fc2b791195e70783d54e1": {"t": 1785758578.8445542, "n": 4697, "h": "cae5bce84634cc0c", "c": "import json\nfrom typing import Any, Dict, Tuple\nfrom fastapi import FastAPI, HTTPException\nfrom jinja2 import Template\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\nfrom json_repair import repair_json\n\n\n# Frontend must provide AI config. Default provider is 'ollama'. No environment fallback is used.\n\n# Native Ollama path removed; we use the OpenAI-compatible API for Ollama\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\nasync def openai_compat_chat(user_prompt: str, ai: Dict) -> Tuple[str, str]:\n \"\"\"\n Call an OpenAI-compatible chat completion endpoint using frontend-provided config.\n Expected ai keys: { provider, api_key }. Model is restricted to provider defaults.\n \"\"\"\n api_key = (ai or {}).get(\"api_key\")\n provider = ((ai or {}).get(\"provider\") or \"ollama\").lower()\n # For OpenAI-compatible providers, api_key is required. For 'ollama' it can be omitted.\n if provider != \"ollama\" and not api_key:\n raise HTTPException(status_code=400, detail=\"ai.api_key is required for non-ollama providers\")\n\n # Provider → base_url + allowed models mapping\n provider_defaults = {\n \"openai\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"models\": [\"o4-mini\"],\n },\n \"gemini\": {\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n \"models\": [\"gemini-2.5-flash\"],\n },\n \"groq\": {\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"models\": [\"llama-3.3-70b-versatile\"],\n },\n \"deepseek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"models\": [\"deepseek-chat\"],\n },\n # Ollama via OpenAI-compatible API\n \"ollama\": {\n \"base_url\": \"http://localhost:11434/v1\",\n \"models\": [\"llama3.2:3b\"],\n },\n }\n\n defaults = provider_defaults.get(provider)\n if not defaults:\n raise HTTPException(status_code=400, detail=\"Unsupported ai.provider\")\n\n base_url = defaults[\"base_url\"]\n allowed_models = defaults.get(\"models\") or []\n if not allowed_models:\n raise HTTPException(status_code=500, detail=\"No models configured for provider\")\n # Use default (first) model; ignore custom model input to restrict surface\n model = allowed_models[0]\n\n try:\n from openai import OpenAI\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"openai client not installed: {str(e)}\")\n\n if provider == \"ollama\" and not api_key:\n api_key = \"ollama\" # placeholder; Ollama ignores the token\n client = OpenAI(api_key=api_key, base_url=base_url)\n try:\n resp = client.chat.completions.create(\n model=model,\n messages=[\n {\"role\": \"user\", \"content\": user_prompt}\n ],\n )\n msg = None\n try:\n msg = (resp.choices[0].message.content if resp and resp.choices else None)\n except Exception:\n pass\n if not msg:\n try:\n msg = resp.choices[0].message # type: ignore\n except Exception:\n msg = None\n text = msg if isinstance(msg, str) else json.dumps(resp.model_dump() if hasattr(resp, \"model_dump\") else resp)\n return text, model\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"OpenAI-compatible chat error: {str(e)}\")\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump()\n }\n prompt = PROMPT_TPL.render(data=payload)\n if not isinstance(req.ai, dict):\n # Default to ollama if not provided\n req.ai = {\"provider\": \"ollama\"}\n print(f\"Prompt sent to {req.ai.get('provider', 'ollama')} (auto):\\n{prompt}\", flush=True)\n data, used_model = await openai_compat_chat(user_prompt=prompt, ai=req.ai or {})\n data = repair_json(data)\n data = json.loads(data)\n\n # Parse JSON\n\n # Sanity: s'assure qu'on renvoie bien le hash de la tx demandée\n data[\"tx_hash\"] = tx.tx_hash\n\n explanations.append(data)\n\n # Model label from enforced provider defaults\n response_model = used_model\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=response_model,\n explanations=[TxExplanation(**e) for e in explanations],\n )"}, "9c1c74f4c13d20d325db8995976805a553a2cef6": {"t": 1785758579.9939106, "n": 1947, "h": "53154510753ccfac", "c": "import os, json, httpx\nfrom typing import Any, Dict\nfrom fastapi import FastAPI, HTTPException\nfrom jinja2 import Template\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\nfrom json_repair import repair_json\n\n\nOLLAMA_BASE_URL = os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434\")\nMODEL = os.getenv(\"MODEL\", \"llama3.2:3b\")\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\nasync def ollama_chat(user_prompt: str) -> str:\n \"\"\"\n Call Ollama chat API. Returns raw model text.\n \"\"\"\n url = f\"{OLLAMA_BASE_URL}/api/generate\"\n payload = {\n \"model\": MODEL,\n \"prompt\": user_prompt,\n \"stream\": False\n }\n print(url, flush=True)\n async with httpx.AsyncClient(timeout=1200.0) as client:\n r = await client.post(url, json=payload)\n if r.status_code != 200:\n raise HTTPException(status_code=500, detail=f\"Ollama error: {r.text}\")\n data = r.json()\n print(data, flush=True)\n return data.get(\"response\", {})\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump()\n }\n prompt = PROMPT_TPL.render(data=payload)\n print(f\"Prompt sent to Ollama:\\n{prompt}\", flush=True)\n # Appel LLM\n data = await ollama_chat(user_prompt=prompt)\n data = repair_json(data)\n data = json.loads(data)\n data[\"tx_hash\"] = tx.tx_hash\n data[\"scores\"] = tx.subscores\n explanations.append(data)\n\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=MODEL,\n explanations=[TxExplanation(**e) for e in explanations],\n )"}, "daa1c5682233922550be3f1371b377eca11bb621": {"t": 1785758581.6287441, "n": 2005, "h": "3c5c440eaeaa72b6", "c": "import os, json, httpx\nfrom typing import Any, Dict\nfrom fastapi import FastAPI, HTTPException\nfrom jinja2 import Template\nfrom schemas import ExplainRequest, ExplainResponse, TxExplanation\nfrom json_repair import repair_json\n\n\nOLLAMA_BASE_URL = os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434\")\nMODEL = os.getenv(\"MODEL\", \"llama3.2:3b\")\n\n\n# Load prompt template\nwith open(\"prompt.jinja\", \"r\", encoding=\"utf-8\") as f:\n PROMPT_TPL = Template(f.read())\n\nasync def ollama_chat(user_prompt: str) -> str:\n \"\"\"\n Call Ollama chat API. Returns raw model text.\n \"\"\"\n url = f\"{OLLAMA_BASE_URL}/api/generate\"\n payload = {\n \"model\": MODEL,\n \"prompt\": user_prompt,\n \"stream\": False\n }\n print(url, flush=True)\n async with httpx.AsyncClient(timeout=1200.0) as client:\n r = await client.post(url, json=payload)\n if r.status_code != 200:\n raise HTTPException(status_code=500, detail=f\"Ollama error: {r.text}\")\n data = r.json()\n print(data, flush=True)\n return data.get(\"response\", {})\n\n\nasync def explain(req: ExplainRequest):\n explanations = []\n\n # On explique TX par TX pour garder des réponses courtes et robustes\n for tx in req.scored_transactions:\n # Construit le prompt\n payload = {\n \"network\": req.network,\n \"address\": req.address,\n \"tx\": tx.model_dump()\n }\n prompt = PROMPT_TPL.render(data=payload)\n print(f\"Prompt sent to Ollama:\\n{prompt}\", flush=True)\n # Appel LLM\n data = await ollama_chat(user_prompt=prompt)\n data = repair_json(data)\n data = json.loads(data)\n\n # Parse JSON\n\n # Sanity: s'assure qu'on renvoie bien le hash de la tx demandée\n data[\"tx_hash\"] = tx.tx_hash\n\n explanations.append(data)\n\n return ExplainResponse(\n network=req.network,\n address=req.address,\n model=MODEL,\n explanations=[TxExplanation(**e) for e in explanations],\n )"}, "1d50a30160f63e28d8cafe783c38f2abbb1b2535": {"t": 1785758582.8665805, "n": 1276, "h": "a05c2a492b273d11", "c": "from fastapi import APIRouter, Query\nimport requests, os, json\nfrom dotenv import load_dotenv\n\n# Création du routeur FastAPI\nrouter = APIRouter()\n\n# Charger le fichier .env (placé dans le dossier back)\nload_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))\n\n@router.get(\"/ai/chat\")\nasync def ai_chat(message: str = Query(..., description=\"Message utilisateur à envoyer à asi1.ai\")):\n \"\"\"\n Envoie un message à l'API asi1.ai et renvoie la réponse du modèle.\n Exemple: GET /ai/chat?message=Bonjour\n \"\"\"\n url = \"https://api.asi1.ai/v1/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {os.getenv('ASI_ONE_API_KEY')}\",\n \"Content-Type\": \"application/json\"\n }\n body = {\n \"model\": \"asi1-mini\",\n \"messages\": [{\"role\": \"user\", \"content\": message}]\n }\n\n try:\n response = requests.post(url, headers=headers, json=body)\n if response.status_code != 200:\n return {\"error\": \"Échec de la requête vers l'API asi1.ai\", \"status_code\": response.status_code}\n\n data = response.json()\n ai_response = data[\"choices\"][0][\"message\"][\"content\"]\n return {\"message_sent\": message, \"response\": ai_response}\n\n except Exception as e:\n return {\"error\": str(e)}\n\n\n"}}}} |