Update app.py
Browse files
app.py
CHANGED
|
@@ -1,19 +1,25 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
gemini-web2api
|
| 4 |
-
========================================
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
ุงูุชุญุณููุงุช ุงูุฌุฏูุฏุฉ:
|
| 8 |
-
- โ
ุฅุตูุงุญ MODELS dict (ุฎุทุฃ syntax ูุงู ู
ูุฌูุฏุงู)
|
| 9 |
- โ
Agent Loop ุญูููู (ReAct: Reason โ Act โ Observe โ Repeat)
|
| 10 |
-
- โ
|
| 11 |
-
- โ
|
| 12 |
-
- โ
|
| 13 |
-
- โ
|
| 14 |
-
- โ
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"""
|
| 18 |
|
| 19 |
import asyncio
|
|
@@ -26,10 +32,9 @@ import ssl
|
|
| 26 |
import sys
|
| 27 |
import uuid
|
| 28 |
import argparse
|
| 29 |
-
import base64
|
| 30 |
import urllib.parse
|
| 31 |
import urllib.request
|
| 32 |
-
from typing import Optional, List, Dict, Any, Tuple
|
| 33 |
from contextlib import asynccontextmanager
|
| 34 |
from dataclasses import dataclass, field
|
| 35 |
|
|
@@ -47,32 +52,58 @@ try:
|
|
| 47 |
except ImportError:
|
| 48 |
HAS_HTTPX = False
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
|
|
|
| 52 |
# โโโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 53 |
|
| 54 |
def load_config() -> dict:
|
| 55 |
defaults = {
|
|
|
|
| 56 |
"port": 7860,
|
| 57 |
"host": "0.0.0.0",
|
| 58 |
-
"
|
| 59 |
-
|
| 60 |
-
|
| 61 |
"gemini_bl": "boq_assistant-bard-web-server_20260525.09_p0",
|
| 62 |
"default_model": "gemini-3.5-flash",
|
| 63 |
-
"log_requests": True,
|
| 64 |
"cookie_file": None,
|
| 65 |
"proxy": None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
"api_keys": ["sk-gemini"],
|
| 67 |
"rate_limit_per_minute": 20,
|
|
|
|
|
|
|
| 68 |
"cache_ttl_seconds": 3600,
|
| 69 |
"db_path": "cache.db",
|
| 70 |
"cleanup_interval_seconds": 300,
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
|
|
|
|
| 76 |
for p in ["./config.json", os.path.expanduser("~/.config/gemini-web2api/config.json")]:
|
| 77 |
if os.path.exists(p):
|
| 78 |
try:
|
|
@@ -80,9 +111,10 @@ def load_config() -> dict:
|
|
| 80 |
defaults.update(json.load(f))
|
| 81 |
print(f"[Config] Loaded: {p}")
|
| 82 |
except Exception as e:
|
| 83 |
-
print(f"[Config] Error
|
| 84 |
break
|
| 85 |
|
|
|
|
| 86 |
if os.environ.get("API_KEYS"):
|
| 87 |
defaults["api_keys"] = [k.strip() for k in os.environ["API_KEYS"].split(",") if k.strip()]
|
| 88 |
if os.environ.get("GEMINI_COOKIE"):
|
|
@@ -91,19 +123,22 @@ def load_config() -> dict:
|
|
| 91 |
defaults["proxy"] = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
|
| 92 |
if os.environ.get("GEMINI_BL"):
|
| 93 |
defaults["gemini_bl"] = os.environ["GEMINI_BL"]
|
|
|
|
|
|
|
| 94 |
|
| 95 |
return defaults
|
| 96 |
|
| 97 |
CONFIG = load_config()
|
| 98 |
|
| 99 |
-
#
|
| 100 |
-
#
|
|
|
|
| 101 |
|
| 102 |
MODELS = {
|
| 103 |
"gemini-3.7-flash": {
|
| 104 |
"mode": 1, "think": 4,
|
| 105 |
"desc": "Fast general-purpose model (~12k chars)",
|
| 106 |
-
},
|
| 107 |
"gemini-3.6-flash": {
|
| 108 |
"mode": 1, "think": 4,
|
| 109 |
"desc": "All-around model (Gemini 3.6 Flash)",
|
|
@@ -114,11 +149,11 @@ MODELS = {
|
|
| 114 |
},
|
| 115 |
"gemini-3.5-flash-thinking": {
|
| 116 |
"mode": 2, "think": 0,
|
| 117 |
-
"desc": "Deep thinking mode
|
| 118 |
},
|
| 119 |
"gemini-3.1-pro": {
|
| 120 |
"mode": 3, "think": 4,
|
| 121 |
-
"desc": "Pro model
|
| 122 |
},
|
| 123 |
"gemini-auto": {
|
| 124 |
"mode": 4, "think": 4,
|
|
@@ -126,16 +161,17 @@ MODELS = {
|
|
| 126 |
},
|
| 127 |
"gemini-3.5-flash-thinking-lite": {
|
| 128 |
"mode": 5, "think": 0,
|
| 129 |
-
"desc": "Dynamic thinking
|
| 130 |
},
|
| 131 |
-
# โ
ุฅุตูุงุญ: ูุงู "claude-opus-4-6" ู
ูุฑุฑุงู ุจุฎุทุฃุ ูุงูุตุญูุญ ูู ูู
ูุฐุฌ Gemini ุฎููู
|
| 132 |
"gemini-flash-lite": {
|
| 133 |
"mode": 6, "think": 4,
|
| 134 |
"desc": "Lightweight fast model (~10k chars)",
|
| 135 |
},
|
| 136 |
}
|
| 137 |
|
|
|
|
| 138 |
# โโโ Pydantic Schemas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 139 |
|
| 140 |
class ChatMessage(BaseModel):
|
| 141 |
role: str
|
|
@@ -154,7 +190,23 @@ class ChatCompletionRequest(BaseModel):
|
|
| 154 |
tool_choice: Optional[Any] = "auto"
|
| 155 |
user: Optional[str] = "default"
|
| 156 |
|
| 157 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
@dataclass
|
| 160 |
class RateLimitEntry:
|
|
@@ -240,15 +292,347 @@ class Services:
|
|
| 240 |
|
| 241 |
services = Services()
|
| 242 |
|
| 243 |
-
#
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
# โโโ Cookie & Headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 252 |
|
| 253 |
def load_cookie() -> Tuple[str, Optional[str]]:
|
| 254 |
inline = CONFIG.get("_cookie_inline", "")
|
|
@@ -301,7 +685,9 @@ def _build_headers() -> dict:
|
|
| 301 |
headers["Authorization"] = make_sapisidhash(sapisid)
|
| 302 |
return headers
|
| 303 |
|
|
|
|
| 304 |
# โโโ Gemini Protocol โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 305 |
|
| 306 |
def _build_gemini_payload(prompt: str, model_id: int, think_mode: int) -> Tuple[bytes, str]:
|
| 307 |
inner = [None] * 80
|
|
@@ -429,7 +815,8 @@ def gemini_stream_iter(prompt: str, model_id: int, think_mode: int):
|
|
| 429 |
inner2 = json.loads(inner_str)
|
| 430 |
if isinstance(inner2, list) and len(inner2) > 4 and inner2[4]:
|
| 431 |
for part in inner2[4]:
|
| 432 |
-
if isinstance(part, list) and len(part) > 1
|
|
|
|
| 433 |
for t in part[1]:
|
| 434 |
if isinstance(t, str) and len(t) > len(prev_text):
|
| 435 |
delta = t[len(prev_text):]
|
|
@@ -440,79 +827,64 @@ def gemini_stream_iter(prompt: str, model_id: int, think_mode: int):
|
|
| 440 |
except (json.JSONDecodeError, IndexError, TypeError):
|
| 441 |
pass
|
| 442 |
|
| 443 |
-
#
|
|
|
|
|
|
|
| 444 |
|
| 445 |
def parse_tool_calls(text: str) -> Tuple[str, Optional[List[dict]]]:
|
| 446 |
"""
|
| 447 |
-
ู
ุญููู Tool Calls ู
ุชุนุฏุฏ ุงูู
ุณุชููุงุช
|
| 448 |
-
1. ```tool_call {...}```
|
| 449 |
-
2. ```json {"name": ..., "arguments": ...}```
|
| 450 |
-
3. <tool_call>...</tool_call>
|
| 451 |
-
4. JSON ู
ุจุงุดุฑ ูู ุงููุต
|
| 452 |
"""
|
| 453 |
tool_calls = []
|
| 454 |
|
| 455 |
-
#
|
| 456 |
-
|
| 457 |
-
for
|
| 458 |
-
|
| 459 |
-
tc = _try_parse_single_tool_call(raw)
|
| 460 |
if tc:
|
| 461 |
tool_calls.append(tc)
|
| 462 |
|
| 463 |
-
#
|
| 464 |
if not tool_calls:
|
| 465 |
-
|
| 466 |
-
for
|
| 467 |
-
|
| 468 |
-
tc = _try_parse_single_tool_call(raw)
|
| 469 |
if tc:
|
| 470 |
tool_calls.append(tc)
|
| 471 |
|
| 472 |
-
#
|
| 473 |
if not tool_calls:
|
| 474 |
-
|
| 475 |
-
for
|
| 476 |
-
|
| 477 |
-
tc = _try_parse_single_tool_call(raw)
|
| 478 |
if tc:
|
| 479 |
tool_calls.append(tc)
|
| 480 |
|
| 481 |
-
#
|
| 482 |
if not tool_calls:
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
tc = _try_parse_single_tool_call(match.group(0))
|
| 487 |
if tc:
|
| 488 |
tool_calls.append(tc)
|
| 489 |
|
| 490 |
if not tool_calls:
|
| 491 |
return text, None
|
| 492 |
|
| 493 |
-
# ุชูุธูู ุงููุต
|
| 494 |
-
clean = re.sub(
|
| 495 |
clean = re.sub(r'```json\s*\n.*?\n?```', '', clean, flags=re.DOTALL)
|
| 496 |
clean = re.sub(r'<tool_call>.*?</tool_call>', '', clean, flags=re.DOTALL)
|
| 497 |
-
# ุฅุฒุงูุฉ ุฃู JSON ู
ุจุงุดุฑ ูุงู tool call
|
| 498 |
-
for tc_obj in tool_calls:
|
| 499 |
-
fn_name = tc_obj["function"]["name"]
|
| 500 |
-
clean = re.sub(
|
| 501 |
-
r'\{[^{}]*"name"\s*:\s*"' + re.escape(fn_name) + r'"[^{}]*\}',
|
| 502 |
-
'', clean
|
| 503 |
-
)
|
| 504 |
|
| 505 |
return clean.strip(), tool_calls
|
| 506 |
|
| 507 |
-
|
| 508 |
-
|
| 509 |
try:
|
| 510 |
data = json.loads(raw)
|
| 511 |
except json.JSONDecodeError:
|
| 512 |
-
# ู
ุญุงููุฉ ุฅุตูุงุญ JSON ุงูู
ูุณูุฑ
|
| 513 |
-
raw_fixed = _fix_json(raw)
|
| 514 |
try:
|
| 515 |
-
data = json.loads(
|
| 516 |
except json.JSONDecodeError:
|
| 517 |
return None
|
| 518 |
|
|
@@ -539,107 +911,108 @@ def _try_parse_single_tool_call(raw: str) -> Optional[dict]:
|
|
| 539 |
},
|
| 540 |
}
|
| 541 |
|
|
|
|
| 542 |
def _fix_json(s: str) -> str:
|
| 543 |
-
"""ุฅุตูุงุญ JSON ุงูู
ูุณูุฑ ุฌุฒุฆูุงู."""
|
| 544 |
-
# ุฅุฒุงูุฉ trailing commas
|
| 545 |
s = re.sub(r',\s*([}\]])', r'\1', s)
|
| 546 |
-
# ุฅุถุงูุฉ quotes ููู
ูุงุชูุญ ุบูุฑ ุงูู
ููุชุจูุณุฉ
|
| 547 |
s = re.sub(r'(\w+)\s*:', r'"\1":', s)
|
| 548 |
-
# ุฅุฒุงูุฉ double quotes
|
| 549 |
s = re.sub(r'""(\w+)""', r'"\1"', s)
|
| 550 |
return s
|
| 551 |
|
| 552 |
-
#
|
|
|
|
|
|
|
| 553 |
|
| 554 |
-
AGENT_SYSTEM_PROMPT = """You are a
|
| 555 |
|
| 556 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
-
|
|
|
|
| 559 |
|
| 560 |
```tool_call
|
| 561 |
{"name": "tool_name_here", "arguments": {"param1": "value1", "param2": "value2"}}
|
| 562 |
```
|
| 563 |
|
| 564 |
-
##
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
5. **Be explicit** - Always explain what tool you're calling and why.
|
| 571 |
|
| 572 |
## AVAILABLE TOOLS
|
| 573 |
|
| 574 |
{tool_definitions}
|
| 575 |
|
| 576 |
-
## RESPONSE
|
| 577 |
-
|
| 578 |
-
-
|
| 579 |
-
- If
|
| 580 |
-
-
|
| 581 |
-
- Never fabricate tool results.
|
| 582 |
-
|
| 583 |
-
## EXAMPLE
|
| 584 |
-
|
| 585 |
-
User: What's the weather in Paris?
|
| 586 |
-
Assistant:
|
| 587 |
-
```tool_call
|
| 588 |
-
{"name": "get_weather", "arguments": {"location": "Paris", "unit": "celsius"}}
|
| 589 |
-
```
|
| 590 |
-
|
| 591 |
-
[After receiving tool result]
|
| 592 |
-
Assistant: Based on the weather data, Paris currently has...
|
| 593 |
"""
|
| 594 |
|
| 595 |
def build_tool_definitions_text(tools: List[dict]) -> str:
|
| 596 |
-
"""ุจูุงุก ูุตู ูุตู ููุฃุฏูุงุช ุงูู
ุชุงุญุฉ."""
|
| 597 |
if not tools:
|
| 598 |
-
return "No tools available."
|
| 599 |
lines = []
|
| 600 |
for i, tool in enumerate(tools, 1):
|
| 601 |
-
if tool.get("type") == "function"
|
| 602 |
-
fn = tool.get("function", tool)
|
| 603 |
-
else:
|
| 604 |
-
fn = tool
|
| 605 |
name = fn.get("name", f"tool_{i}")
|
| 606 |
desc = fn.get("description", "No description")
|
| 607 |
params = fn.get("parameters", {})
|
| 608 |
-
|
| 609 |
lines.append(f"### {i}. `{name}`")
|
| 610 |
lines.append(f"**Description:** {desc}")
|
| 611 |
-
|
| 612 |
if params and params.get("properties"):
|
| 613 |
lines.append("**Parameters:**")
|
| 614 |
-
for pname, pinfo in params
|
| 615 |
-
|
| 616 |
ptype = pinfo.get("type", "any")
|
| 617 |
pdesc = pinfo.get("description", "")
|
| 618 |
-
|
| 619 |
-
lines.append(f" - `{pname}` ({ptype}){req_str}: {pdesc}")
|
| 620 |
lines.append("")
|
| 621 |
return "\n".join(lines)
|
| 622 |
|
|
|
|
| 623 |
# โโโ Prompt Builder โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 624 |
|
| 625 |
-
def messages_to_prompt(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
"""
|
| 627 |
-
|
|
|
|
|
|
|
|
|
|
| 628 |
"""
|
| 629 |
parts = []
|
| 630 |
|
| 631 |
-
# โโโ
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
if tools:
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
parts.append(f"[SYSTEM]\n{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
-
# โโโ ู
ุนุงูุฌุฉ
|
| 638 |
for msg in messages:
|
| 639 |
role = msg.get("role", "user") if isinstance(msg, dict) else msg.role
|
| 640 |
content = msg.get("content", "") if isinstance(msg, dict) else (msg.content or "")
|
| 641 |
-
|
| 642 |
-
else getattr(msg, "tool_calls", None))
|
| 643 |
|
| 644 |
# ุชุญููู content ู
ุชุนุฏุฏ ุงูุฃุฌุฒุงุก
|
| 645 |
if isinstance(content, list):
|
|
@@ -647,125 +1020,127 @@ def messages_to_prompt(messages: list, tools: list = None) -> str:
|
|
| 647 |
c.get("text", "") for c in content
|
| 648 |
if isinstance(c, dict) and c.get("type") in ("text", "input_text")
|
| 649 |
)
|
|
|
|
| 650 |
|
| 651 |
if role == "system":
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
|
|
|
|
|
|
| 657 |
|
| 658 |
elif role == "assistant":
|
| 659 |
-
if
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
f"```tool_call\n"
|
| 666 |
-
f"{json.dumps({'name': fn.get('name'), 'arguments': json.loads(fn.get('arguments', '{}'))}, ensure_ascii=False)}\n"
|
| 667 |
-
f"```"
|
| 668 |
-
)
|
| 669 |
if content:
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
|
|
|
| 673 |
parts.append(f"[ASSISTANT]: {content}")
|
| 674 |
|
| 675 |
elif role == "tool":
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
tool_call_id = (msg.get("tool_call_id", "") if isinstance(msg, dict)
|
| 680 |
-
else getattr(msg, "tool_call_id", ""))
|
| 681 |
-
parts.append(
|
| 682 |
-
f"[TOOL RESULT - {tool_name or tool_call_id}]:\n```json\n{content}\n```\n"
|
| 683 |
-
f"Now use this result to continue."
|
| 684 |
-
)
|
| 685 |
-
|
| 686 |
-
else: # user
|
| 687 |
if content:
|
| 688 |
-
parts.append(
|
|
|
|
|
|
|
|
|
|
| 689 |
|
| 690 |
-
return "\n\n".join(p for p in parts if p)
|
| 691 |
|
| 692 |
-
#
|
|
|
|
|
|
|
| 693 |
|
| 694 |
def run_agent_sync(
|
| 695 |
messages: List[dict],
|
| 696 |
model_id: int,
|
| 697 |
think_mode: int,
|
| 698 |
tools: Optional[List[dict]],
|
|
|
|
| 699 |
max_iterations: int = None,
|
| 700 |
) -> Tuple[str, Optional[List[dict]], List[dict]]:
|
| 701 |
"""
|
| 702 |
-
Agent Loop
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
4. ุฅุฐุง ูู
ููุนุฏ tool call โ ุฃุนุฏ ุงูุฑุฏ ุงูููุงุฆู
|
| 707 |
|
| 708 |
-
ูุนูุฏ
|
|
|
|
|
|
|
| 709 |
"""
|
| 710 |
if max_iterations is None:
|
| 711 |
max_iterations = CONFIG["agent_max_iterations"]
|
| 712 |
|
| 713 |
current_messages = list(messages)
|
| 714 |
-
iteration = 0
|
| 715 |
-
last_tool_calls = None
|
| 716 |
|
| 717 |
-
|
| 718 |
-
iteration += 1
|
| 719 |
log(f"Agent iteration {iteration}/{max_iterations}", "AGENT")
|
| 720 |
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
|
| 725 |
if not text:
|
| 726 |
log("Empty response from Gemini", "WARN")
|
|
|
|
|
|
|
| 727 |
break
|
| 728 |
|
| 729 |
-
#
|
| 730 |
clean_text, tool_calls = parse_tool_calls(text)
|
| 731 |
|
| 732 |
if tool_calls and tools:
|
| 733 |
-
|
|
|
|
| 734 |
|
| 735 |
-
# ุฃุถู ุฑุฏ ุงูู
ุณุงุนุฏ
|
| 736 |
current_messages.append({
|
| 737 |
"role": "assistant",
|
| 738 |
"content": clean_text or None,
|
| 739 |
"tool_calls": tool_calls,
|
| 740 |
})
|
| 741 |
-
last_tool_calls = tool_calls
|
| 742 |
|
| 743 |
-
#
|
| 744 |
-
|
| 745 |
-
# ูู ูุถุน agent ุฏุงุฎููุ ูุถูู placeholder
|
| 746 |
-
return clean_text, tool_calls, current_messages
|
| 747 |
|
| 748 |
else:
|
| 749 |
-
#
|
| 750 |
-
log(f"Final
|
| 751 |
return clean_text or text, None, current_messages
|
| 752 |
|
| 753 |
log(f"Max iterations ({max_iterations}) reached", "WARN")
|
| 754 |
-
return text or "", None, current_messages
|
| 755 |
|
| 756 |
|
| 757 |
-
def
|
| 758 |
messages: List[dict],
|
| 759 |
model_id: int,
|
| 760 |
think_mode: int,
|
| 761 |
tools: Optional[List[dict]],
|
| 762 |
-
tool_executor
|
|
|
|
| 763 |
max_iterations: int = None,
|
| 764 |
) -> Tuple[str, Optional[List[dict]], List[dict]]:
|
| 765 |
"""
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
tool_executor: Callable[[str, dict], str] - ูุฃุฎุฐ (tool_name, args) ูููุนูุฏ ูุชูุฌุฉ ูุตูุฉ
|
| 769 |
"""
|
| 770 |
if max_iterations is None:
|
| 771 |
max_iterations = CONFIG["agent_max_iterations"]
|
|
@@ -773,35 +1148,31 @@ def run_agent_with_tool_execution_sync(
|
|
| 773 |
current_messages = list(messages)
|
| 774 |
|
| 775 |
for iteration in range(1, max_iterations + 1):
|
| 776 |
-
log(f"Agent
|
| 777 |
|
| 778 |
-
prompt = messages_to_prompt(current_messages, tools)
|
| 779 |
try:
|
| 780 |
raw = gemini_stream_generate(prompt, model_id, think_mode)
|
| 781 |
text = extract_response_text(raw)
|
| 782 |
except Exception as e:
|
| 783 |
-
log(f"Gemini
|
| 784 |
return f"Error: {e}", None, current_messages
|
| 785 |
|
| 786 |
if not text:
|
| 787 |
-
log("Empty response", "WARN")
|
| 788 |
break
|
| 789 |
|
| 790 |
clean_text, tool_calls = parse_tool_calls(text)
|
| 791 |
|
| 792 |
if not tool_calls or not tools:
|
| 793 |
-
|
| 794 |
-
log(f"โ
Final answer after {iteration} iterations", "AGENT")
|
| 795 |
return clean_text or text, None, current_messages
|
| 796 |
|
| 797 |
-
# โโโ ุฃุถู ุฑุฏ ุงูู
ุณุงุนุฏ
|
| 798 |
current_messages.append({
|
| 799 |
"role": "assistant",
|
| 800 |
"content": clean_text or None,
|
| 801 |
"tool_calls": tool_calls,
|
| 802 |
})
|
| 803 |
|
| 804 |
-
# โโโ ุชูููุฐ ูู tool call
|
| 805 |
for tc in tool_calls:
|
| 806 |
fn_name = tc["function"]["name"]
|
| 807 |
try:
|
|
@@ -809,7 +1180,7 @@ def run_agent_with_tool_execution_sync(
|
|
| 809 |
except Exception:
|
| 810 |
fn_args = {}
|
| 811 |
|
| 812 |
-
log(f"Executing
|
| 813 |
|
| 814 |
if tool_executor:
|
| 815 |
try:
|
|
@@ -817,10 +1188,9 @@ def run_agent_with_tool_execution_sync(
|
|
| 817 |
result_str = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
|
| 818 |
except Exception as e:
|
| 819 |
result_str = json.dumps({"error": str(e)})
|
| 820 |
-
log(f"Tool
|
| 821 |
else:
|
| 822 |
-
|
| 823 |
-
result_str = json.dumps({"status": "tool_executed", "note": "No executor provided"})
|
| 824 |
|
| 825 |
current_messages.append({
|
| 826 |
"role": "tool",
|
|
@@ -829,18 +1199,16 @@ def run_agent_with_tool_execution_sync(
|
|
| 829 |
"content": result_str,
|
| 830 |
})
|
| 831 |
|
| 832 |
-
log(f"Tools executed, continuing loop...", "AGENT")
|
| 833 |
-
|
| 834 |
-
log(f"Max iterations reached", "WARN")
|
| 835 |
return text or "", None, current_messages
|
| 836 |
|
| 837 |
|
| 838 |
-
# โโโ Sync wrapper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 839 |
-
|
| 840 |
def call_gemini_sync(
|
| 841 |
-
prompt: str,
|
|
|
|
|
|
|
|
|
|
| 842 |
) -> Tuple[str, Optional[list]]:
|
| 843 |
-
"""ุงุณุชุฏุนุงุก
|
| 844 |
raw = gemini_stream_generate(prompt, model_id, think_mode)
|
| 845 |
text = extract_response_text(raw)
|
| 846 |
tool_calls = None
|
|
@@ -848,7 +1216,9 @@ def call_gemini_sync(
|
|
| 848 |
text, tool_calls = parse_tool_calls(text)
|
| 849 |
return text or "", tool_calls
|
| 850 |
|
|
|
|
| 851 |
# โโโ Model Resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 852 |
|
| 853 |
def resolve_model(model_name: str) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[str]]:
|
| 854 |
think_override = None
|
|
@@ -858,9 +1228,9 @@ def resolve_model(model_name: str) -> Tuple[Optional[str], Optional[int], Option
|
|
| 858 |
think_override = int(think_str)
|
| 859 |
except ValueError:
|
| 860 |
pass
|
|
|
|
| 861 |
cfg = MODELS.get(model_name)
|
| 862 |
if not cfg:
|
| 863 |
-
# fallback ูููู
ูุฐุฌ ุงูุงูุชุฑุงุถู ุจุฏูุงู ู
ู ุงูุฎุทุฃ ุงูู
ุจุงุดุฑ
|
| 864 |
default = CONFIG.get("default_model", "gemini-3.5-flash")
|
| 865 |
cfg = MODELS.get(default)
|
| 866 |
if cfg:
|
|
@@ -868,14 +1238,56 @@ def resolve_model(model_name: str) -> Tuple[Optional[str], Optional[int], Option
|
|
| 868 |
model_name = default
|
| 869 |
else:
|
| 870 |
return None, None, None, f"Unknown model: {model_name}. Available: {list(MODELS.keys())}"
|
|
|
|
| 871 |
return model_name, cfg["mode"], (think_override if think_override is not None else cfg["think"]), None
|
| 872 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
# โโโ Authentication โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 874 |
|
| 875 |
security = HTTPBearer(auto_error=False)
|
| 876 |
|
| 877 |
async def verify_api_key(
|
| 878 |
-
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
| 879 |
) -> str:
|
| 880 |
api_keys = CONFIG.get("api_keys", [])
|
| 881 |
if not api_keys:
|
|
@@ -887,21 +1299,25 @@ async def verify_api_key(
|
|
| 887 |
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 888 |
return token
|
| 889 |
|
|
|
|
| 890 |
# โโโ FastAPI App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 891 |
|
| 892 |
@asynccontextmanager
|
| 893 |
async def lifespan(app: FastAPI):
|
| 894 |
await services.init_db()
|
| 895 |
await services.start_cleanup()
|
|
|
|
| 896 |
log(f"gemini-web2api v{__version__} started", "INFO")
|
|
|
|
| 897 |
yield
|
| 898 |
if services._cleanup_task:
|
| 899 |
services._cleanup_task.cancel()
|
| 900 |
|
| 901 |
app = FastAPI(
|
| 902 |
-
title="Gemini Web2API (Agent Edition)",
|
| 903 |
version=__version__,
|
| 904 |
-
description="OpenAI-compatible proxy
|
| 905 |
lifespan=lifespan,
|
| 906 |
)
|
| 907 |
|
|
@@ -913,7 +1329,9 @@ app.add_middleware(
|
|
| 913 |
allow_headers=["*"],
|
| 914 |
)
|
| 915 |
|
|
|
|
| 916 |
# โโโ /v1/models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 917 |
|
| 918 |
@app.get("/v1/models")
|
| 919 |
async def list_models():
|
|
@@ -930,14 +1348,13 @@ async def list_models():
|
|
| 930 |
"tool_use": True,
|
| 931 |
"streaming": True,
|
| 932 |
"agent_loop": True,
|
|
|
|
| 933 |
},
|
| 934 |
}
|
| 935 |
for name, cfg in MODELS.items()
|
| 936 |
],
|
| 937 |
}
|
| 938 |
|
| 939 |
-
# โโโ /v1beta/models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 940 |
-
|
| 941 |
@app.get("/v1beta/models")
|
| 942 |
async def google_models_list():
|
| 943 |
return {
|
|
@@ -952,7 +1369,9 @@ async def google_models_list():
|
|
| 952 |
]
|
| 953 |
}
|
| 954 |
|
|
|
|
| 955 |
# โโโ /v1/chat/completions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 956 |
|
| 957 |
@app.post("/v1/chat/completions")
|
| 958 |
async def chat_completions(
|
|
@@ -960,7 +1379,7 @@ async def chat_completions(
|
|
| 960 |
request: Request,
|
| 961 |
_: str = Depends(verify_api_key),
|
| 962 |
):
|
| 963 |
-
# Rate Limiting
|
| 964 |
client_ip = request.client.host
|
| 965 |
allowed, wait_time = await services.check_rate_limit(client_ip)
|
| 966 |
if not allowed:
|
|
@@ -970,6 +1389,7 @@ async def chat_completions(
|
|
| 970 |
headers={"Retry-After": str(wait_time)},
|
| 971 |
)
|
| 972 |
|
|
|
|
| 973 |
model_name, model_id, think_mode, err = resolve_model(
|
| 974 |
body.model or CONFIG["default_model"]
|
| 975 |
)
|
|
@@ -978,75 +1398,94 @@ async def chat_completions(
|
|
| 978 |
|
| 979 |
tools = body.tools
|
| 980 |
messages = [m.dict(exclude_none=True) for m in body.messages]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
|
| 982 |
created = int(time.time())
|
| 983 |
|
| 984 |
-
# Cache key
|
| 985 |
-
context_str = "".join(f"{m.get('role')}:{m.get('content')}" for m in messages)
|
| 986 |
cache_key = hashlib.md5(
|
| 987 |
-
f"{model_name}:{
|
| 988 |
).hexdigest()
|
| 989 |
|
| 990 |
-
# โโโ Streaming โโโ
|
| 991 |
if body.stream:
|
| 992 |
async def stream_gen():
|
| 993 |
full_text = ""
|
|
|
|
| 994 |
try:
|
| 995 |
if tools:
|
| 996 |
-
# โ
Streaming ู
ุน ุฃุฏูุงุช: ููุฑุณู tool calls ูู chunk ุฎุงุต
|
| 997 |
loop = asyncio.get_event_loop()
|
| 998 |
-
text,
|
| 999 |
None, run_agent_sync,
|
| 1000 |
messages, model_id, think_mode, tools,
|
| 1001 |
-
CONFIG["agent_max_iterations"]
|
| 1002 |
)
|
| 1003 |
full_text = text or ""
|
|
|
|
| 1004 |
|
| 1005 |
-
if
|
| 1006 |
# ุฅุฑุณุงู tool calls chunk
|
| 1007 |
-
msg_delta = {"role": "assistant", "content": full_text or None, "tool_calls": tool_calls}
|
| 1008 |
chunk = {
|
| 1009 |
"id": cid, "object": "chat.completion.chunk",
|
| 1010 |
"created": created, "model": model_name,
|
| 1011 |
-
"choices": [{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
}
|
| 1013 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1014 |
else:
|
| 1015 |
-
# ุฅุฑุณุงู ุงููุต ุชุฏุฑูุฌูุงู
|
| 1016 |
if full_text:
|
| 1017 |
chunk = {
|
| 1018 |
"id": cid, "object": "chat.completion.chunk",
|
| 1019 |
"created": created, "model": model_name,
|
| 1020 |
-
"choices": [{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
}
|
| 1022 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1023 |
|
| 1024 |
else:
|
| 1025 |
# Streaming ุจุฏูู ุฃุฏูุงุช
|
| 1026 |
-
prompt = messages_to_prompt(messages)
|
| 1027 |
for delta in gemini_stream_iter(prompt, model_id, think_mode):
|
| 1028 |
full_text += delta
|
| 1029 |
chunk = {
|
| 1030 |
"id": cid, "object": "chat.completion.chunk",
|
| 1031 |
"created": created, "model": model_name,
|
| 1032 |
-
"choices": [{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
}
|
| 1034 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1035 |
|
| 1036 |
# Final chunk
|
| 1037 |
-
finish = "tool_calls" if
|
| 1038 |
-
|
| 1039 |
-
"id": cid, "object": "chat.completion.chunk",
|
| 1040 |
-
"created": created, "model": model_name,
|
| 1041 |
-
"choices": [{"index": 0, "delta": {}, "finish_reason": finish}],
|
| 1042 |
-
}
|
| 1043 |
-
yield f"data: {json.dumps(final_chunk)}\n\n"
|
| 1044 |
yield "data: [DONE]\n\n"
|
| 1045 |
|
| 1046 |
-
#
|
| 1047 |
-
if full_text and not
|
| 1048 |
-
|
| 1049 |
-
await
|
| 1050 |
|
| 1051 |
except Exception as e:
|
| 1052 |
log(f"Stream error: {e}", "ERROR")
|
|
@@ -1054,8 +1493,8 @@ async def chat_completions(
|
|
| 1054 |
|
| 1055 |
return StreamingResponse(stream_gen(), media_type="text/event-stream")
|
| 1056 |
|
| 1057 |
-
# โโโ Non-streaming โโโ
|
| 1058 |
-
# Cache (
|
| 1059 |
if not tools:
|
| 1060 |
cached = await services.get_cache(cache_key)
|
| 1061 |
if cached:
|
|
@@ -1064,52 +1503,37 @@ async def chat_completions(
|
|
| 1064 |
try:
|
| 1065 |
loop = asyncio.get_event_loop()
|
| 1066 |
if tools:
|
| 1067 |
-
|
| 1068 |
-
text, tool_calls, _ = await loop.run_in_executor(
|
| 1069 |
None, run_agent_sync,
|
| 1070 |
messages, model_id, think_mode, tools,
|
| 1071 |
-
CONFIG["agent_max_iterations"]
|
| 1072 |
)
|
| 1073 |
else:
|
| 1074 |
-
prompt = messages_to_prompt(messages)
|
| 1075 |
text, tool_calls = await loop.run_in_executor(
|
| 1076 |
-
None, call_gemini_sync,
|
|
|
|
| 1077 |
)
|
| 1078 |
except Exception as e:
|
| 1079 |
log(f"Error: {e}", "ERROR")
|
| 1080 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1081 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1082 |
prompt_text = messages_to_prompt(messages)
|
| 1083 |
resp_data = _build_completion_response(cid, created, model_name, text, tool_calls, prompt_text)
|
| 1084 |
|
| 1085 |
-
if not tools:
|
| 1086 |
await services.set_cache(cache_key, json.dumps(resp_data))
|
| 1087 |
|
| 1088 |
return JSONResponse(content=resp_data)
|
| 1089 |
|
| 1090 |
-
|
| 1091 |
-
def _build_completion_response(
|
| 1092 |
-
cid: str, created: int, model: str, text: str,
|
| 1093 |
-
tool_calls: Optional[list], prompt: str = ""
|
| 1094 |
-
) -> dict:
|
| 1095 |
-
msg = {"role": "assistant", "content": text or None}
|
| 1096 |
-
if tool_calls:
|
| 1097 |
-
msg["tool_calls"] = tool_calls
|
| 1098 |
-
finish = "tool_calls" if tool_calls else "stop"
|
| 1099 |
-
return {
|
| 1100 |
-
"id": cid,
|
| 1101 |
-
"object": "chat.completion",
|
| 1102 |
-
"created": created,
|
| 1103 |
-
"model": model,
|
| 1104 |
-
"choices": [{"index": 0, "message": msg, "finish_reason": finish}],
|
| 1105 |
-
"usage": {
|
| 1106 |
-
"prompt_tokens": len(prompt) // 4,
|
| 1107 |
-
"completion_tokens": len(text) // 4 if text else 0,
|
| 1108 |
-
"total_tokens": (len(prompt) + len(text)) // 4 if text else len(prompt) // 4,
|
| 1109 |
-
},
|
| 1110 |
-
}
|
| 1111 |
-
|
| 1112 |
# โโโ /v1/responses (Codex CLI / Responses API) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1113 |
|
| 1114 |
@app.post("/v1/responses")
|
| 1115 |
async def responses_api(
|
|
@@ -1126,6 +1550,7 @@ async def responses_api(
|
|
| 1126 |
input_items = req.get("input", [])
|
| 1127 |
tools = req.get("tools")
|
| 1128 |
messages = []
|
|
|
|
| 1129 |
|
| 1130 |
if req.get("instructions"):
|
| 1131 |
messages.append({"role": "system", "content": req["instructions"]})
|
|
@@ -1197,21 +1622,32 @@ async def responses_api(
|
|
| 1197 |
if not any(m.get("content") for m in messages):
|
| 1198 |
raise HTTPException(status_code=400, detail="Empty input")
|
| 1199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
loop = asyncio.get_event_loop()
|
| 1201 |
try:
|
| 1202 |
if tools:
|
| 1203 |
text, tool_calls, _ = await loop.run_in_executor(
|
| 1204 |
-
None, run_agent_sync,
|
| 1205 |
-
|
|
|
|
| 1206 |
)
|
| 1207 |
else:
|
| 1208 |
-
prompt = messages_to_prompt(messages)
|
| 1209 |
text, tool_calls = await loop.run_in_executor(
|
| 1210 |
None, call_gemini_sync, prompt, model_id, think_mode, None
|
| 1211 |
)
|
| 1212 |
except Exception as e:
|
| 1213 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
rid = f"resp_{uuid.uuid4().hex[:16]}"
|
| 1216 |
mid = f"msg_{uuid.uuid4().hex[:12]}"
|
| 1217 |
output = []
|
|
@@ -1236,6 +1672,7 @@ async def responses_api(
|
|
| 1236 |
})
|
| 1237 |
|
| 1238 |
prompt_text = messages_to_prompt(messages)
|
|
|
|
| 1239 |
if req.get("stream"):
|
| 1240 |
async def responses_stream():
|
| 1241 |
yield f"event: response.created\ndata: {json.dumps({'type': 'response.created', 'response': {'id': rid, 'object': 'response', 'status': 'in_progress', 'model': model_name, 'output': []}})}\n\n"
|
|
@@ -1268,38 +1705,44 @@ async def responses_api(
|
|
| 1268 |
},
|
| 1269 |
})
|
| 1270 |
|
|
|
|
| 1271 |
# โโโ Google Native API /v1beta โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1272 |
|
| 1273 |
-
def
|
| 1274 |
-
|
| 1275 |
sys_inst = req.get("systemInstruction")
|
| 1276 |
if sys_inst:
|
| 1277 |
sys_text = " ".join(p.get("text", "") for p in sys_inst.get("parts", []) if p.get("text"))
|
| 1278 |
if sys_text:
|
| 1279 |
-
|
| 1280 |
for content in req.get("contents", []):
|
| 1281 |
-
role = content.get("role"
|
| 1282 |
text = " ".join(p.get("text", "") for p in content.get("parts", []) if p.get("text"))
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
else:
|
| 1286 |
-
parts.append(f"[USER]: {text}")
|
| 1287 |
-
return "\n\n".join(p for p in parts if p)
|
| 1288 |
|
| 1289 |
@app.post("/v1beta/models/{model_path:path}")
|
| 1290 |
async def google_generate(model_path: str, request: Request):
|
| 1291 |
req = await request.json()
|
| 1292 |
model_name_raw = model_path.split(":")[0]
|
| 1293 |
-
|
| 1294 |
|
| 1295 |
model_name, model_id, think_mode, err = resolve_model(model_name_raw)
|
| 1296 |
if err:
|
| 1297 |
raise HTTPException(status_code=400, detail=err)
|
| 1298 |
|
| 1299 |
-
|
| 1300 |
-
if not
|
| 1301 |
raise HTTPException(status_code=400, detail="Empty content")
|
| 1302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1303 |
loop = asyncio.get_event_loop()
|
| 1304 |
try:
|
| 1305 |
text, _ = await loop.run_in_executor(
|
|
@@ -1308,6 +1751,9 @@ async def google_generate(model_path: str, request: Request):
|
|
| 1308 |
except Exception as e:
|
| 1309 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1310 |
|
|
|
|
|
|
|
|
|
|
| 1311 |
candidate = {
|
| 1312 |
"content": {"parts": [{"text": text or ""}], "role": "model"},
|
| 1313 |
"finishReason": "STOP",
|
|
@@ -1323,14 +1769,16 @@ async def google_generate(model_path: str, request: Request):
|
|
| 1323 |
"modelVersion": model_name,
|
| 1324 |
}
|
| 1325 |
|
| 1326 |
-
if
|
| 1327 |
async def google_stream():
|
| 1328 |
yield f"data: {json.dumps(response_obj)}\n\n"
|
| 1329 |
return StreamingResponse(google_stream(), media_type="text/event-stream")
|
| 1330 |
|
| 1331 |
return JSONResponse(response_obj)
|
| 1332 |
|
|
|
|
| 1333 |
# โโโ Health & Root โโโโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1334 |
|
| 1335 |
@app.get("/")
|
| 1336 |
async def root():
|
|
@@ -1338,24 +1786,77 @@ async def root():
|
|
| 1338 |
"service": "gemini-web2api",
|
| 1339 |
"version": __version__,
|
| 1340 |
"models": list(MODELS.keys()),
|
|
|
|
| 1341 |
"features": {
|
|
|
|
| 1342 |
"agent_loop": True,
|
| 1343 |
"tool_calling": True,
|
| 1344 |
"streaming": True,
|
| 1345 |
"cache": True,
|
| 1346 |
"rate_limiting": True,
|
| 1347 |
},
|
| 1348 |
-
"endpoints": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
}
|
| 1350 |
|
| 1351 |
@app.get("/health")
|
| 1352 |
async def health():
|
| 1353 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1355 |
# โโโ Gradio UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1356 |
|
| 1357 |
def build_gradio_interface():
|
| 1358 |
-
async def chat_handler(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1359 |
if not message or not message.strip():
|
| 1360 |
yield history
|
| 1361 |
return
|
|
@@ -1366,71 +1867,121 @@ def build_gradio_interface():
|
|
| 1366 |
yield history
|
| 1367 |
return
|
| 1368 |
|
| 1369 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
yield history
|
| 1371 |
|
| 1372 |
full_response = ""
|
| 1373 |
try:
|
| 1374 |
-
|
|
|
|
|
|
|
| 1375 |
for chunk in gemini_stream_iter(prompt, model_id, think_mode):
|
| 1376 |
full_response += chunk
|
| 1377 |
history[-1] = (message, full_response)
|
| 1378 |
yield history
|
|
|
|
| 1379 |
except Exception as e:
|
| 1380 |
full_response = f"โ Error: {e}"
|
| 1381 |
history[-1] = (message, full_response)
|
| 1382 |
yield history
|
|
|
|
| 1383 |
|
| 1384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1385 |
api_key_display = CONFIG.get("api_keys", ["sk-gemini"])[0]
|
| 1386 |
|
| 1387 |
-
with gr.Blocks(
|
|
|
|
|
|
|
|
|
|
| 1388 |
gr.Markdown(f"""
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
""")
|
| 1399 |
|
| 1400 |
with gr.Row():
|
| 1401 |
with gr.Column(scale=4):
|
| 1402 |
-
chatbot = gr.Chatbot(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1403 |
with gr.Column(scale=1):
|
| 1404 |
-
gr.Markdown("""
|
| 1405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
- Tool Calling ู
ูุซูู
|
| 1407 |
-
- JSON Parser ู
ุชุนุฏุฏ ุงูู
ุณุชููุงุช
|
| 1408 |
- Streaming ุญูููู
|
| 1409 |
-
- SQLite Cache
|
| 1410 |
-
- Rate Limiting
|
| 1411 |
-
- Retry Logic
|
| 1412 |
-
- Fallback ูููู
ูุฐุฌ ุงูุงูุชุฑุงุถู
|
| 1413 |
""")
|
| 1414 |
|
| 1415 |
with gr.Row():
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
model_dd = gr.Dropdown(
|
| 1422 |
choices=list(MODELS.keys()),
|
| 1423 |
value=CONFIG["default_model"],
|
| 1424 |
-
label="ุงููู
ูุฐุฌ",
|
| 1425 |
scale=2,
|
| 1426 |
)
|
| 1427 |
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1434 |
clear_btn.click(lambda: [], None, [chatbot])
|
| 1435 |
|
| 1436 |
return demo
|
|
@@ -1438,17 +1989,20 @@ def build_gradio_interface():
|
|
| 1438 |
demo = build_gradio_interface()
|
| 1439 |
app = gr.mount_gradio_app(app, demo, path="/chat")
|
| 1440 |
|
|
|
|
| 1441 |
# โโโ Entry Point โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 1442 |
|
| 1443 |
if __name__ == "__main__":
|
| 1444 |
import uvicorn
|
| 1445 |
|
| 1446 |
-
parser = argparse.ArgumentParser(description="Gemini Web2API
|
| 1447 |
parser.add_argument("--port", type=int, default=CONFIG["port"])
|
| 1448 |
parser.add_argument("--host", type=str, default=CONFIG["host"])
|
| 1449 |
parser.add_argument("--cookie-file", type=str, default=None)
|
| 1450 |
parser.add_argument("--proxy", type=str, default=None)
|
| 1451 |
parser.add_argument("--max-iterations", type=int, default=CONFIG["agent_max_iterations"])
|
|
|
|
| 1452 |
parser.add_argument("--version", action="version", version=f"v{__version__}")
|
| 1453 |
args = parser.parse_args()
|
| 1454 |
|
|
@@ -1456,18 +2010,23 @@ if __name__ == "__main__":
|
|
| 1456 |
CONFIG["cookie_file"] = args.cookie_file
|
| 1457 |
if args.proxy:
|
| 1458 |
CONFIG["proxy"] = args.proxy
|
|
|
|
|
|
|
| 1459 |
CONFIG["agent_max_iterations"] = args.max_iterations
|
| 1460 |
|
| 1461 |
-
print(f"\n{'
|
| 1462 |
-
print(f" gemini-web2api v{__version__} โ Agent
|
| 1463 |
-
print(f"{'
|
| 1464 |
print(f" API: http://localhost:{args.port}/v1")
|
| 1465 |
print(f" Chat UI: http://localhost:{args.port}/chat")
|
|
|
|
|
|
|
| 1466 |
print(f" Models: {', '.join(MODELS.keys())}")
|
| 1467 |
print(f" Agent Loop: max {CONFIG['agent_max_iterations']} iterations")
|
|
|
|
| 1468 |
print(f" Cookie: {'โ
' if CONFIG.get('cookie_file') or CONFIG.get('_cookie_inline') else 'โ none (anonymous)'}")
|
| 1469 |
print(f" Proxy: {CONFIG.get('proxy') or 'โ none'}")
|
| 1470 |
-
print(f"{'
|
| 1471 |
|
| 1472 |
uvicorn.run(
|
| 1473 |
app,
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
gemini-web2api + Zep Cloud Memory โ Professional Agent Edition
|
| 4 |
+
===============================================================
|
| 5 |
+
ูุณุฎุฉ ุงุญุชุฑุงููุฉ ูุงู
ูุฉ ู
ุน:
|
| 6 |
+
- โ
Zep Cloud: ุฐุงูุฑุฉ ุฏุงุฆู
ุฉ ุนุจุฑ ูู ุงูุฌูุณุงุช (ุญูุงุฆู + ู
ูุฎุต + ุจุญุซ ุฏูุงูู)
|
|
|
|
|
|
|
| 7 |
- โ
Agent Loop ุญูููู (ReAct: Reason โ Act โ Observe โ Repeat)
|
| 8 |
+
- โ
Tool Calling ู
ูุซูู ู
ุน JSON parser ู
ุชุนุฏุฏ ุงูู
ุณุชููุงุช
|
| 9 |
+
- โ
Streaming ุญูููู ู
ุน Tool Calls
|
| 10 |
+
- โ
SQLite Cache + Rate Limiting
|
| 11 |
+
- โ
Retry Logic + Fallback ูููู
ูุฐุฌ ุงูุงูุชุฑุงุถู
|
| 12 |
+
- โ
Gradio UI ู
ุญุณูู
|
| 13 |
+
|
| 14 |
+
ุงูู
ุชุทูุจุงุช:
|
| 15 |
+
pip install fastapi uvicorn aiosqlite httpx gradio zep-cloud pydantic
|
| 16 |
+
|
| 17 |
+
ู
ุชุบูุฑุงุช ุงูุจูุฆุฉ:
|
| 18 |
+
ZEP_API_KEY - ู
ูุชุงุญ Zep Cloud (ู
ุทููุจ ููุฐุงูุฑุฉ ุงูุฏุงุฆู
ุฉ)
|
| 19 |
+
API_KEYS - ู
ูุงุชูุญ API ู
ูุตููุฉ ุจูุงุตูุฉ (ุงูุชุฑุงุถู: sk-gemini)
|
| 20 |
+
GEMINI_COOKIE - ูููู Gemini (ุงุฎุชูุงุฑูุ ููุญุณูู ุงูุฌูุฏุฉ)
|
| 21 |
+
HTTPS_PROXY - ุจุฑููุณู (ุงุฎุชูุงุฑู)
|
| 22 |
+
GEMINI_BL - Gemini BL header (ุงุฎุชูุงุฑู)
|
| 23 |
"""
|
| 24 |
|
| 25 |
import asyncio
|
|
|
|
| 32 |
import sys
|
| 33 |
import uuid
|
| 34 |
import argparse
|
|
|
|
| 35 |
import urllib.parse
|
| 36 |
import urllib.request
|
| 37 |
+
from typing import Optional, List, Dict, Any, Tuple, Callable
|
| 38 |
from contextlib import asynccontextmanager
|
| 39 |
from dataclasses import dataclass, field
|
| 40 |
|
|
|
|
| 52 |
except ImportError:
|
| 53 |
HAS_HTTPX = False
|
| 54 |
|
| 55 |
+
try:
|
| 56 |
+
from zep_cloud.client import AsyncZep
|
| 57 |
+
from zep_cloud.types import Message as ZepMessage
|
| 58 |
+
HAS_ZEP = True
|
| 59 |
+
except ImportError:
|
| 60 |
+
HAS_ZEP = False
|
| 61 |
+
|
| 62 |
+
__version__ = "3.0.0-zep"
|
| 63 |
|
| 64 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 65 |
# โโโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 66 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 67 |
|
| 68 |
def load_config() -> dict:
|
| 69 |
defaults = {
|
| 70 |
+
# โโโ Server
|
| 71 |
"port": 7860,
|
| 72 |
"host": "0.0.0.0",
|
| 73 |
+
"log_requests": True,
|
| 74 |
+
|
| 75 |
+
# โโโ Gemini
|
| 76 |
"gemini_bl": "boq_assistant-bard-web-server_20260525.09_p0",
|
| 77 |
"default_model": "gemini-3.5-flash",
|
|
|
|
| 78 |
"cookie_file": None,
|
| 79 |
"proxy": None,
|
| 80 |
+
"retry_attempts": 3,
|
| 81 |
+
"retry_delay_sec": 2,
|
| 82 |
+
"request_timeout_sec": 180,
|
| 83 |
+
|
| 84 |
+
# โโโ API
|
| 85 |
"api_keys": ["sk-gemini"],
|
| 86 |
"rate_limit_per_minute": 20,
|
| 87 |
+
|
| 88 |
+
# โโโ Cache
|
| 89 |
"cache_ttl_seconds": 3600,
|
| 90 |
"db_path": "cache.db",
|
| 91 |
"cleanup_interval_seconds": 300,
|
| 92 |
+
|
| 93 |
+
# โโโ Agent Loop
|
| 94 |
+
"agent_max_iterations": 10,
|
| 95 |
+
"agent_max_tokens_per_step": 4096,
|
| 96 |
+
|
| 97 |
+
# โโโ Zep Cloud Memory
|
| 98 |
+
"zep_api_key": os.environ.get("ZEP_API_KEY", ""),
|
| 99 |
+
"zep_memory_type": "perpetual", # perpetual | summary_retriever
|
| 100 |
+
"zep_min_fact_rating": 0.3, # ุญุฏ ุฃุฏูู ูุฌูุฏุฉ ุงูุญูุงุฆู
|
| 101 |
+
"zep_recent_messages": 20, # ุนุฏุฏ ุงูุฑุณุงุฆู ุงูุฃุฎูุฑุฉ ูู ุงูุณูุงู
|
| 102 |
+
"zep_search_limit": 5, # ูุชุงุฆุฌ ุงูุจุญุซ ุงูุฏูุงูู
|
| 103 |
+
"zep_save_system_msgs": False, # ูู ุชุญูุธ ุฑุณุงุฆู ุงููุธุงู
ุ
|
| 104 |
}
|
| 105 |
|
| 106 |
+
# ุชุญู
ูู ู
ูู ุงูุฅุนุฏุงุฏุงุช ุฅุฐุง ููุฌุฏ
|
| 107 |
for p in ["./config.json", os.path.expanduser("~/.config/gemini-web2api/config.json")]:
|
| 108 |
if os.path.exists(p):
|
| 109 |
try:
|
|
|
|
| 111 |
defaults.update(json.load(f))
|
| 112 |
print(f"[Config] Loaded: {p}")
|
| 113 |
except Exception as e:
|
| 114 |
+
print(f"[Config] Error: {e}")
|
| 115 |
break
|
| 116 |
|
| 117 |
+
# ู
ุชุบูุฑุงุช ุงูุจูุฆุฉ ุชููุบู ุงูุฅุนุฏุงุฏุงุช
|
| 118 |
if os.environ.get("API_KEYS"):
|
| 119 |
defaults["api_keys"] = [k.strip() for k in os.environ["API_KEYS"].split(",") if k.strip()]
|
| 120 |
if os.environ.get("GEMINI_COOKIE"):
|
|
|
|
| 123 |
defaults["proxy"] = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
|
| 124 |
if os.environ.get("GEMINI_BL"):
|
| 125 |
defaults["gemini_bl"] = os.environ["GEMINI_BL"]
|
| 126 |
+
if os.environ.get("ZEP_API_KEY"):
|
| 127 |
+
defaults["zep_api_key"] = os.environ["ZEP_API_KEY"]
|
| 128 |
|
| 129 |
return defaults
|
| 130 |
|
| 131 |
CONFIG = load_config()
|
| 132 |
|
| 133 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 134 |
+
# โโโ Models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 135 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 136 |
|
| 137 |
MODELS = {
|
| 138 |
"gemini-3.7-flash": {
|
| 139 |
"mode": 1, "think": 4,
|
| 140 |
"desc": "Fast general-purpose model (~12k chars)",
|
| 141 |
+
},
|
| 142 |
"gemini-3.6-flash": {
|
| 143 |
"mode": 1, "think": 4,
|
| 144 |
"desc": "All-around model (Gemini 3.6 Flash)",
|
|
|
|
| 149 |
},
|
| 150 |
"gemini-3.5-flash-thinking": {
|
| 151 |
"mode": 2, "think": 0,
|
| 152 |
+
"desc": "Deep thinking mode (~20k chars)",
|
| 153 |
},
|
| 154 |
"gemini-3.1-pro": {
|
| 155 |
"mode": 3, "think": 4,
|
| 156 |
+
"desc": "Pro model",
|
| 157 |
},
|
| 158 |
"gemini-auto": {
|
| 159 |
"mode": 4, "think": 4,
|
|
|
|
| 161 |
},
|
| 162 |
"gemini-3.5-flash-thinking-lite": {
|
| 163 |
"mode": 5, "think": 0,
|
| 164 |
+
"desc": "Dynamic thinking (~15k chars)",
|
| 165 |
},
|
|
|
|
| 166 |
"gemini-flash-lite": {
|
| 167 |
"mode": 6, "think": 4,
|
| 168 |
"desc": "Lightweight fast model (~10k chars)",
|
| 169 |
},
|
| 170 |
}
|
| 171 |
|
| 172 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 173 |
# โโโ Pydantic Schemas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 174 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 175 |
|
| 176 |
class ChatMessage(BaseModel):
|
| 177 |
role: str
|
|
|
|
| 190 |
tool_choice: Optional[Any] = "auto"
|
| 191 |
user: Optional[str] = "default"
|
| 192 |
|
| 193 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 194 |
+
# โโโ Logging โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 195 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 196 |
+
|
| 197 |
+
def log(msg: str, level: str = "INFO"):
|
| 198 |
+
if CONFIG.get("log_requests"):
|
| 199 |
+
icons = {
|
| 200 |
+
"INFO": "โน๏ธ", "AGENT": "๐ค", "TOOL": "๐ง",
|
| 201 |
+
"ERROR": "โ", "WARN": "โ ๏ธ", "MEM": "๐ง ",
|
| 202 |
+
}
|
| 203 |
+
icon = icons.get(level, "โข")
|
| 204 |
+
sys.stderr.write(f"[{time.strftime('%H:%M:%S')}] {icon} {msg}\n")
|
| 205 |
+
sys.stderr.flush()
|
| 206 |
+
|
| 207 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 208 |
+
# โโโ Services (Cache + Rate Limit) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 209 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 210 |
|
| 211 |
@dataclass
|
| 212 |
class RateLimitEntry:
|
|
|
|
| 292 |
|
| 293 |
services = Services()
|
| 294 |
|
| 295 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 296 |
+
# โโโ Zep Cloud Memory Manager โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 297 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 298 |
|
| 299 |
+
class ZepMemoryManager:
|
| 300 |
+
"""
|
| 301 |
+
ู
ุฏูุฑ ุงูุฐุงูุฑุฉ ุงูุฏุงุฆู
ุฉ ุนุจุฑ Zep Cloud.
|
| 302 |
+
|
| 303 |
+
ุงูู
ูุฒุงุช:
|
| 304 |
+
โโโโโโโโโ
|
| 305 |
+
โข ูู user_id โ ุฌูุณุฉ (session) ุซุงุจุชุฉ ูุง ุชูู
ุณุญ ุจูู ุงูุฏุฑุฏุดุงุช
|
| 306 |
+
โข ูุณุชุฑุฌุน: ุงูุญูุงุฆู ุงูู
ุณุชุฎุฑุฌุฉ + ู
ูุฎุต ุงูู
ุญุงุฏุซุฉ + ุขุฎุฑ N ุฑุณุงูุฉ
|
| 307 |
+
โข ูุจุญุซ ุฏูุงููุงู ูู ูุงู
ู ุชุงุฑูุฎ ุงูู
ุญุงุฏุซุฉ ุจูุงุกู ุนูู ุงูุณุคุงู ุงูุญุงูู
|
| 308 |
+
โข ูุญูุธ ูู ุฑุณุงูุฉ (user + assistant + tool) ุชููุงุฆูุงู ุจุนุฏ ูู ุฑุฏ
|
| 309 |
+
โข ูุชุนุงู
ู ู
ุน ุชุบููุฑ ุงูู
ูุถูุน: ูู ุฑุณุงูุฉ ู
ุฑุชุจุทุฉ ุจู timestamp ูุณูุงู
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
def __init__(self):
|
| 313 |
+
self.client: Optional[AsyncZep] = None
|
| 314 |
+
self._enabled = False
|
| 315 |
+
self._init_error: str = ""
|
| 316 |
+
|
| 317 |
+
async def init(self):
|
| 318 |
+
"""ุชููุฆุฉ ุนู
ูู Zep Cloud."""
|
| 319 |
+
if not HAS_ZEP:
|
| 320 |
+
self._init_error = "zep-cloud not installed. Run: pip install zep-cloud"
|
| 321 |
+
log(self._init_error, "WARN")
|
| 322 |
+
return
|
| 323 |
+
|
| 324 |
+
api_key = CONFIG.get("zep_api_key", "").strip()
|
| 325 |
+
if not api_key:
|
| 326 |
+
self._init_error = "ZEP_API_KEY not set โ persistent memory disabled"
|
| 327 |
+
log(self._init_error, "WARN")
|
| 328 |
+
return
|
| 329 |
+
|
| 330 |
+
try:
|
| 331 |
+
self.client = AsyncZep(api_key=api_key)
|
| 332 |
+
# ุงุฎุชุจุงุฑ ุงูุงุชุตุงู
|
| 333 |
+
await self.client.user.list(page_number=1, page_size=1)
|
| 334 |
+
self._enabled = True
|
| 335 |
+
log("โ
Zep Cloud memory connected and ready", "MEM")
|
| 336 |
+
except Exception as e:
|
| 337 |
+
self._init_error = f"Zep Cloud connection failed: {e}"
|
| 338 |
+
log(self._init_error, "ERROR")
|
| 339 |
+
|
| 340 |
+
@property
|
| 341 |
+
def enabled(self) -> bool:
|
| 342 |
+
return self._enabled and self.client is not None
|
| 343 |
+
|
| 344 |
+
# โโโ Session Management โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 345 |
+
|
| 346 |
+
def _session_id(self, user_id: str) -> str:
|
| 347 |
+
"""
|
| 348 |
+
session_id ุซุงุจุช ูู
ุจูู ุนูู user_id.
|
| 349 |
+
ูุง ูุชุบูุฑ ุจูู ุงูุฌูุณุงุช โ ูุฐุง ูู ู
ูุชุงุญ ุงูุงุณุชู
ุฑุงุฑูุฉ.
|
| 350 |
+
"""
|
| 351 |
+
safe = re.sub(r'[^a-zA-Z0-9_-]', '_', user_id)
|
| 352 |
+
return f"agent_{safe}"
|
| 353 |
+
|
| 354 |
+
async def ensure_user_session(self, user_id: str) -> bool:
|
| 355 |
+
"""
|
| 356 |
+
ููุดุฆ ุงูู
ุณุชุฎุฏู
ูุงูุฌูุณุฉ ูู Zep ุฅู ูู
ุชูููุง ู
ูุฌูุฏููู.
|
| 357 |
+
ูุนูุฏ True ุนูุฏ ุงููุฌุงุญ.
|
| 358 |
+
"""
|
| 359 |
+
if not self.enabled:
|
| 360 |
+
return False
|
| 361 |
+
try:
|
| 362 |
+
session_id = self._session_id(user_id)
|
| 363 |
+
|
| 364 |
+
# ุฅูุดุงุก ุงูู
ุณุชุฎุฏู
(ูุชุฌุงูู ุงูุฎุทุฃ ุฅุฐุง ูุงู ู
ูุฌูุฏุงู)
|
| 365 |
+
try:
|
| 366 |
+
await self.client.user.add(
|
| 367 |
+
user_id=user_id,
|
| 368 |
+
metadata={
|
| 369 |
+
"source": "gemini-web2api",
|
| 370 |
+
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 371 |
+
}
|
| 372 |
+
)
|
| 373 |
+
log(f"Created Zep user: {user_id}", "MEM")
|
| 374 |
+
except Exception:
|
| 375 |
+
pass # ุงูู
ุณุชุฎุฏู
ู
ูุฌูุฏ ุจุงููุนู
|
| 376 |
+
|
| 377 |
+
# ุฅูุดุงุก ุงูุฌูุณุฉ (ูุชุฌุงูู ุงูุฎุทุฃ ุฅุฐุง ูุงูุช ู
ูุฌูุฏุฉ)
|
| 378 |
+
try:
|
| 379 |
+
await self.client.memory.add_session(
|
| 380 |
+
session_id=session_id,
|
| 381 |
+
user_id=user_id,
|
| 382 |
+
metadata={
|
| 383 |
+
"agent_version": __version__,
|
| 384 |
+
"model": CONFIG.get("default_model"),
|
| 385 |
+
}
|
| 386 |
+
)
|
| 387 |
+
log(f"Created Zep session: {session_id}", "MEM")
|
| 388 |
+
except Exception:
|
| 389 |
+
pass # ุงูุฌูุณุฉ ู
ูุฌูุฏุฉ ุจุงููุนู
|
| 390 |
+
|
| 391 |
+
return True
|
| 392 |
+
except Exception as e:
|
| 393 |
+
log(f"ensure_user_session error: {e}", "WARN")
|
| 394 |
+
return False
|
| 395 |
+
|
| 396 |
+
# โโโ Memory Retrieval โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 397 |
+
|
| 398 |
+
async def get_full_context(self, user_id: str, current_query: str = "") -> str:
|
| 399 |
+
"""
|
| 400 |
+
ูุณุชุฑุฌุน ุงูุณูุงู ุงููุงู
ู ู
ู Zep ูููุนูุฏู ููุต ู
ูุณููู.
|
| 401 |
+
|
| 402 |
+
ูุดู
ู:
|
| 403 |
+
1. ุงูุญูุงุฆู ุงูู
ุณุชุฎุฑุฌุฉ ุชููุงุฆูุงู (ู
ุซู: ุงุณู
ุงูู
ุณุชุฎุฏู
ุ ุชูุถููุงุชูุ ู
ูุงู
ู)
|
| 404 |
+
2. ู
ูุฎุต ุชููุงุฆู ููู ุงูู
ุญุงุฏุซุงุช ุงูุณุงุจูุฉ
|
| 405 |
+
3. ุขุฎุฑ N ุฑุณุงูุฉ ููุณูุงู ุงูููุฑู
|
| 406 |
+
4. ูุชุงุฆุฌ ุจุญุซ ุฏูุงูู ุฅุฐุง ูุงู ููุงู ุงุณุชุนูุงู
|
| 407 |
+
|
| 408 |
+
ูุฐุง ูุถู
ู:
|
| 409 |
+
- ุชุฐูุฑ ุชุบููุฑุงุช ุงูู
ูุถูุน (ูู ุดูุก ู
ูุฏูููู)
|
| 410 |
+
- ุงูุญูุงุธ ุนูู ุงูุณูุงู ูู ุงูู
ุญุงุฏุซุงุช ุงูุทูููุฉ ุฌุฏุงู
|
| 411 |
+
- ุงุณุชุฑุฌุงุน ุงูู
ุนููู
ุงุช ุฐุงุช ุงูุตูุฉ ุญุชู ู
ู ู
ุญุงุฏุซุงุช ูุฏูู
ุฉ
|
| 412 |
+
"""
|
| 413 |
+
if not self.enabled:
|
| 414 |
+
return ""
|
| 415 |
+
|
| 416 |
+
session_id = self._session_id(user_id)
|
| 417 |
+
parts = []
|
| 418 |
+
|
| 419 |
+
try:
|
| 420 |
+
# โโโ 1. ุงูุฐุงูุฑุฉ ุงูุฃุณุงุณูุฉ (ุญูุงุฆู + ู
ูุฎุต + ุฑุณุงุฆู ุฃุฎูุฑุฉ)
|
| 421 |
+
memory = await self.client.memory.get(
|
| 422 |
+
session_id=session_id,
|
| 423 |
+
min_rating=CONFIG.get("zep_min_fact_rating", 0.3),
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# ุงูุญูุงุฆู ุงูู
ุณุชุฎุฑุฌุฉ ุชููุงุฆูุงู ู
ู ูู ุงูู
ุญุงุฏุซุงุช
|
| 427 |
+
if hasattr(memory, 'facts') and memory.facts:
|
| 428 |
+
facts_lines = []
|
| 429 |
+
for f in memory.facts:
|
| 430 |
+
fact_text = f if isinstance(f, str) else getattr(f, 'fact', str(f))
|
| 431 |
+
if fact_text:
|
| 432 |
+
facts_lines.append(f" โข {fact_text}")
|
| 433 |
+
if facts_lines:
|
| 434 |
+
parts.append(
|
| 435 |
+
"### KNOWN FACTS (extracted automatically from all past conversations)\n"
|
| 436 |
+
+ "\n".join(facts_lines)
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
# ุงูู
ูุฎุต ุงูุชููุงุฆู ููู
ุญุงุฏุซุงุช ุงูุณุงุจูุฉ
|
| 440 |
+
if hasattr(memory, 'summary') and memory.summary:
|
| 441 |
+
summary_text = (
|
| 442 |
+
memory.summary if isinstance(memory.summary, str)
|
| 443 |
+
else getattr(memory.summary, 'content', '')
|
| 444 |
+
)
|
| 445 |
+
if summary_text and summary_text.strip():
|
| 446 |
+
parts.append(
|
| 447 |
+
"### CONVERSATION HISTORY SUMMARY\n"
|
| 448 |
+
+ summary_text.strip()
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
# ุขุฎุฑ ุงูุฑุณุงุฆู ููุณูุงู ุงูููุฑู
|
| 452 |
+
if hasattr(memory, 'messages') and memory.messages:
|
| 453 |
+
n = CONFIG.get("zep_recent_messages", 20)
|
| 454 |
+
recent = memory.messages[-n:] if len(memory.messages) > n else memory.messages
|
| 455 |
+
if recent:
|
| 456 |
+
lines = []
|
| 457 |
+
for m in recent:
|
| 458 |
+
role = getattr(m, 'role_type', getattr(m, 'role', 'user'))
|
| 459 |
+
content = getattr(m, 'content', '') or ''
|
| 460 |
+
if content.strip():
|
| 461 |
+
lines.append(f" [{role.upper()}]: {content.strip()}")
|
| 462 |
+
if lines:
|
| 463 |
+
parts.append(
|
| 464 |
+
f"### RECENT MESSAGES (last {len(recent)})\n"
|
| 465 |
+
+ "\n".join(lines)
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
except Exception as e:
|
| 469 |
+
log(f"get_memory error: {e}", "WARN")
|
| 470 |
+
|
| 471 |
+
# โโโ 2. ุงูุจุญุซ ุงูุฏูุงูู ุจูุงุกู ุนูู ุงูุงุณุชุนูุงู
ุงูุญุงูู
|
| 472 |
+
if current_query and current_query.strip():
|
| 473 |
+
try:
|
| 474 |
+
search_results = await self.client.memory.search_sessions(
|
| 475 |
+
text=current_query,
|
| 476 |
+
user_id=user_id,
|
| 477 |
+
limit=CONFIG.get("zep_search_limit", 5),
|
| 478 |
+
)
|
| 479 |
+
if search_results and hasattr(search_results, 'results') and search_results.results:
|
| 480 |
+
relevant_lines = []
|
| 481 |
+
for r in search_results.results:
|
| 482 |
+
msg = getattr(r, 'message', None)
|
| 483 |
+
if msg:
|
| 484 |
+
content = getattr(msg, 'content', '') or ''
|
| 485 |
+
role = getattr(msg, 'role_type', 'unknown')
|
| 486 |
+
score = getattr(r, 'score', 0)
|
| 487 |
+
if content.strip() and score > 0.5:
|
| 488 |
+
relevant_lines.append(
|
| 489 |
+
f" [{role.upper()}] (relevance: {score:.2f}): {content.strip()}"
|
| 490 |
+
)
|
| 491 |
+
if relevant_lines:
|
| 492 |
+
parts.append(
|
| 493 |
+
"### SEMANTICALLY RELEVANT PAST CONTEXT (for current query)\n"
|
| 494 |
+
+ "\n".join(relevant_lines)
|
| 495 |
+
)
|
| 496 |
+
except Exception as e:
|
| 497 |
+
log(f"semantic search error: {e}", "WARN")
|
| 498 |
+
|
| 499 |
+
if not parts:
|
| 500 |
+
return ""
|
| 501 |
+
|
| 502 |
+
# โโโ ุจูุงุก ุงูุณูุงู ุงูููุงุฆู
|
| 503 |
+
context = (
|
| 504 |
+
"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n"
|
| 505 |
+
"โ PERSISTENT MEMORY โ CROSS-SESSION CONTEXT โ\n"
|
| 506 |
+
"โ Use this to maintain full continuity across all chats โ\n"
|
| 507 |
+
"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n"
|
| 508 |
+
+ "\n\n".join(parts)
|
| 509 |
+
+ "\n\n[END OF PERSISTENT MEMORY]"
|
| 510 |
+
)
|
| 511 |
+
log(f"Retrieved memory context ({len(context)} chars) for user={user_id}", "MEM")
|
| 512 |
+
return context
|
| 513 |
+
|
| 514 |
+
# โโโ Memory Saving โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 515 |
+
|
| 516 |
+
async def save_turn(self, user_id: str, messages: List[dict], assistant_reply: str = ""):
|
| 517 |
+
"""
|
| 518 |
+
ูุญูุธ ุฏูุฑุงู ุงูู
ุญุงุฏุซุฉ (ุฑุณุงุฆู ุงูู
ุณุชุฎุฏู
+ ุฑุฏ ุงูู
ุณุงุนุฏ) ูู Zep.
|
| 519 |
+
|
| 520 |
+
ููุณุชุฏุนู ุจุนุฏ ูู ุฑุฏ ูุงุฌุญ.
|
| 521 |
+
Zep ูุณุชุฎุฑุฌ ุงูุญูุงุฆู ูููุญุฏูุซ ุงูู
ูุฎุต ุชููุงุฆูุงู ูู ุงูุฎูููุฉ.
|
| 522 |
+
"""
|
| 523 |
+
if not self.enabled:
|
| 524 |
+
return
|
| 525 |
+
|
| 526 |
+
session_id = self._session_id(user_id)
|
| 527 |
+
zep_msgs = []
|
| 528 |
+
|
| 529 |
+
try:
|
| 530 |
+
for msg in messages:
|
| 531 |
+
role = msg.get("role", "user")
|
| 532 |
+
content = msg.get("content") or ""
|
| 533 |
+
|
| 534 |
+
# ุชุฌุงูู ุฑุณุงุฆู ุงููุธุงู
ุฅุฐุง ูุงูุช ุงูุฅุนุฏุงุฏุงุช ุชู
ูุนูุง
|
| 535 |
+
if role == "system" and not CONFIG.get("zep_save_system_msgs"):
|
| 536 |
+
continue
|
| 537 |
+
|
| 538 |
+
# ุชุญููู content ุงููุงุฆู
ุฉ ุฅูู ูุต
|
| 539 |
+
if isinstance(content, list):
|
| 540 |
+
content = " ".join(
|
| 541 |
+
c.get("text", "") for c in content
|
| 542 |
+
if isinstance(c, dict) and c.get("type") in ("text", "input_text")
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
content = (content or "").strip()
|
| 546 |
+
if not content:
|
| 547 |
+
continue
|
| 548 |
+
|
| 549 |
+
# ุชุญููู tool calls ุฅูู ูุต ูุงุจู ูููุฑุงุกุฉ
|
| 550 |
+
tool_calls_data = msg.get("tool_calls")
|
| 551 |
+
if tool_calls_data and role == "assistant":
|
| 552 |
+
tc_summary = json.dumps(
|
| 553 |
+
[{"tool": tc["function"]["name"], "args": tc["function"].get("arguments", "{}")}
|
| 554 |
+
for tc in tool_calls_data],
|
| 555 |
+
ensure_ascii=False
|
| 556 |
+
)
|
| 557 |
+
content = f"{content}\n[Tool calls: {tc_summary}]" if content else f"[Tool calls: {tc_summary}]"
|
| 558 |
+
|
| 559 |
+
role_map = {
|
| 560 |
+
"user": "user",
|
| 561 |
+
"assistant": "assistant",
|
| 562 |
+
"tool": "tool",
|
| 563 |
+
"system": "system",
|
| 564 |
+
}
|
| 565 |
+
zep_role = role_map.get(role, "user")
|
| 566 |
+
|
| 567 |
+
zep_msgs.append(
|
| 568 |
+
ZepMessage(
|
| 569 |
+
role_type=zep_role,
|
| 570 |
+
role=role,
|
| 571 |
+
content=content,
|
| 572 |
+
)
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
# ุฅุถุงูุฉ ุฑุฏ ุงูู
ุณุงุนุฏ ุฅุฐุง ูู
ููู ูู ุงููุงุฆู
ุฉ
|
| 576 |
+
if assistant_reply and assistant_reply.strip():
|
| 577 |
+
already_saved = any(
|
| 578 |
+
m.get("role") == "assistant" and (m.get("content") or "") == assistant_reply
|
| 579 |
+
for m in messages
|
| 580 |
+
)
|
| 581 |
+
if not already_saved:
|
| 582 |
+
zep_msgs.append(
|
| 583 |
+
ZepMessage(
|
| 584 |
+
role_type="assistant",
|
| 585 |
+
role="assistant",
|
| 586 |
+
content=assistant_reply.strip(),
|
| 587 |
+
)
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
if zep_msgs:
|
| 591 |
+
await self.client.memory.add(
|
| 592 |
+
session_id=session_id,
|
| 593 |
+
messages=zep_msgs,
|
| 594 |
+
)
|
| 595 |
+
log(f"๐พ Saved {len(zep_msgs)} messages to Zep (user={user_id})", "MEM")
|
| 596 |
+
|
| 597 |
+
except Exception as e:
|
| 598 |
+
log(f"save_turn error: {e}", "WARN")
|
| 599 |
+
|
| 600 |
+
async def add_fact(self, user_id: str, fact: str):
|
| 601 |
+
"""
|
| 602 |
+
ุฅุถุงูุฉ ุญูููุฉ ุตุฑูุญุฉ ูุฐูุฑูุง ุงูู
ุณุชุฎุฏู
(ููุฃุดูุงุก ุงูู
ูู
ุฉ ุฌุฏุงู).
|
| 603 |
+
Zep ูุณุชุฎุฑุฌ ุงูุญูุงุฆู ุชููุงุฆูุงู ุฃูุถุงูุ ููู ูุฐุง ููุชุฃููุฏ.
|
| 604 |
+
"""
|
| 605 |
+
if not self.enabled:
|
| 606 |
+
return
|
| 607 |
+
try:
|
| 608 |
+
session_id = self._session_id(user_id)
|
| 609 |
+
await self.client.memory.add(
|
| 610 |
+
session_id=session_id,
|
| 611 |
+
messages=[
|
| 612 |
+
ZepMessage(
|
| 613 |
+
role_type="system",
|
| 614 |
+
role="system",
|
| 615 |
+
content=f"[IMPORTANT FACT]: {fact}",
|
| 616 |
+
)
|
| 617 |
+
]
|
| 618 |
+
)
|
| 619 |
+
log(f"Added explicit fact for user={user_id}: {fact[:50]}...", "MEM")
|
| 620 |
+
except Exception as e:
|
| 621 |
+
log(f"add_fact error: {e}", "WARN")
|
| 622 |
|
| 623 |
+
def status(self) -> dict:
|
| 624 |
+
return {
|
| 625 |
+
"enabled": self.enabled,
|
| 626 |
+
"provider": "Zep Cloud",
|
| 627 |
+
"error": self._init_error if not self.enabled else None,
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
# ุงูู
ุซูู ุงูุนุงู
|
| 631 |
+
zep_memory = ZepMemoryManager()
|
| 632 |
+
|
| 633 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 634 |
# โโโ Cookie & Headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 635 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 636 |
|
| 637 |
def load_cookie() -> Tuple[str, Optional[str]]:
|
| 638 |
inline = CONFIG.get("_cookie_inline", "")
|
|
|
|
| 685 |
headers["Authorization"] = make_sapisidhash(sapisid)
|
| 686 |
return headers
|
| 687 |
|
| 688 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 689 |
# โโโ Gemini Protocol โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 690 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 691 |
|
| 692 |
def _build_gemini_payload(prompt: str, model_id: int, think_mode: int) -> Tuple[bytes, str]:
|
| 693 |
inner = [None] * 80
|
|
|
|
| 815 |
inner2 = json.loads(inner_str)
|
| 816 |
if isinstance(inner2, list) and len(inner2) > 4 and inner2[4]:
|
| 817 |
for part in inner2[4]:
|
| 818 |
+
if (isinstance(part, list) and len(part) > 1
|
| 819 |
+
and part[1] and isinstance(part[1], list)):
|
| 820 |
for t in part[1]:
|
| 821 |
if isinstance(t, str) and len(t) > len(prev_text):
|
| 822 |
delta = t[len(prev_text):]
|
|
|
|
| 827 |
except (json.JSONDecodeError, IndexError, TypeError):
|
| 828 |
pass
|
| 829 |
|
| 830 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 831 |
+
# โโโ Tool Call Parser โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโ
|
| 832 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 833 |
|
| 834 |
def parse_tool_calls(text: str) -> Tuple[str, Optional[List[dict]]]:
|
| 835 |
"""
|
| 836 |
+
ู
ุญููู Tool Calls ู
ุชุนุฏุฏ ุงูู
ุณุชููุงุช (4 ุฃูู
ุงุท).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
"""
|
| 838 |
tool_calls = []
|
| 839 |
|
| 840 |
+
# ุงููู
ุท 1: ```tool_call {...}```
|
| 841 |
+
p1 = r'```tool_call\s*\n(.*?)\n?```'
|
| 842 |
+
for m in re.finditer(p1, text, re.DOTALL):
|
| 843 |
+
tc = _try_parse_tc(m.group(1).strip())
|
|
|
|
| 844 |
if tc:
|
| 845 |
tool_calls.append(tc)
|
| 846 |
|
| 847 |
+
# ุงููู
ุท 2: ```json {...}``` (ูุญุชูู ุนูู name + arguments)
|
| 848 |
if not tool_calls:
|
| 849 |
+
p2 = r'```json\s*\n(.*?)\n?```'
|
| 850 |
+
for m in re.finditer(p2, text, re.DOTALL):
|
| 851 |
+
tc = _try_parse_tc(m.group(1).strip())
|
|
|
|
| 852 |
if tc:
|
| 853 |
tool_calls.append(tc)
|
| 854 |
|
| 855 |
+
# ุงููู
ุท 3: <tool_call>...</tool_call>
|
| 856 |
if not tool_calls:
|
| 857 |
+
p3 = r'<tool_call>(.*?)</tool_call>'
|
| 858 |
+
for m in re.finditer(p3, text, re.DOTALL):
|
| 859 |
+
tc = _try_parse_tc(m.group(1).strip())
|
|
|
|
| 860 |
if tc:
|
| 861 |
tool_calls.append(tc)
|
| 862 |
|
| 863 |
+
# ุงููู
ุท 4: JSON ู
ุจุงุดุฑ
|
| 864 |
if not tool_calls:
|
| 865 |
+
jp = r'\{[^{}]*"name"\s*:\s*"[^"]+[^{}]*"arguments"\s*:[^{}]*\}'
|
| 866 |
+
for m in re.finditer(jp, text, re.DOTALL):
|
| 867 |
+
tc = _try_parse_tc(m.group(0))
|
|
|
|
| 868 |
if tc:
|
| 869 |
tool_calls.append(tc)
|
| 870 |
|
| 871 |
if not tool_calls:
|
| 872 |
return text, None
|
| 873 |
|
| 874 |
+
# ุชูุธูู ุงููุต
|
| 875 |
+
clean = re.sub(p1, '', text, flags=re.DOTALL)
|
| 876 |
clean = re.sub(r'```json\s*\n.*?\n?```', '', clean, flags=re.DOTALL)
|
| 877 |
clean = re.sub(r'<tool_call>.*?</tool_call>', '', clean, flags=re.DOTALL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
return clean.strip(), tool_calls
|
| 880 |
|
| 881 |
+
|
| 882 |
+
def _try_parse_tc(raw: str) -> Optional[dict]:
|
| 883 |
try:
|
| 884 |
data = json.loads(raw)
|
| 885 |
except json.JSONDecodeError:
|
|
|
|
|
|
|
| 886 |
try:
|
| 887 |
+
data = json.loads(_fix_json(raw))
|
| 888 |
except json.JSONDecodeError:
|
| 889 |
return None
|
| 890 |
|
|
|
|
| 911 |
},
|
| 912 |
}
|
| 913 |
|
| 914 |
+
|
| 915 |
def _fix_json(s: str) -> str:
|
|
|
|
|
|
|
| 916 |
s = re.sub(r',\s*([}\]])', r'\1', s)
|
|
|
|
| 917 |
s = re.sub(r'(\w+)\s*:', r'"\1":', s)
|
|
|
|
| 918 |
s = re.sub(r'""(\w+)""', r'"\1"', s)
|
| 919 |
return s
|
| 920 |
|
| 921 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 922 |
+
# โโโ Agent System Prompt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 923 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 924 |
|
| 925 |
+
AGENT_SYSTEM_PROMPT = """You are a professional AI assistant with persistent memory and access to tools.
|
| 926 |
|
| 927 |
+
## MEMORY INSTRUCTIONS
|
| 928 |
+
- The [PERSISTENT MEMORY] block above contains everything you remember about this user from ALL previous conversations.
|
| 929 |
+
- Use this memory to maintain full continuity even when topics change suddenly.
|
| 930 |
+
- If the user references something from a past conversation, look it up in the memory block first.
|
| 931 |
+
- Facts, preferences, tasks, and details from previous chats are all available to you.
|
| 932 |
|
| 933 |
+
## TOOL USAGE
|
| 934 |
+
When you need to call a tool, use this EXACT format:
|
| 935 |
|
| 936 |
```tool_call
|
| 937 |
{"name": "tool_name_here", "arguments": {"param1": "value1", "param2": "value2"}}
|
| 938 |
```
|
| 939 |
|
| 940 |
+
### Rules:
|
| 941 |
+
1. Always use tools when they are needed โ do not guess information a tool can provide.
|
| 942 |
+
2. Call tools one at a time and wait for the result before calling the next.
|
| 943 |
+
3. Incorporate tool results into your final answer.
|
| 944 |
+
4. Never fabricate tool results.
|
| 945 |
+
5. If a tool fails, explain what happened and try an alternative approach.
|
|
|
|
| 946 |
|
| 947 |
## AVAILABLE TOOLS
|
| 948 |
|
| 949 |
{tool_definitions}
|
| 950 |
|
| 951 |
+
## RESPONSE STYLE
|
| 952 |
+
- Be thorough and professional in task execution.
|
| 953 |
+
- Acknowledge when you remember something from a previous conversation.
|
| 954 |
+
- If topics change mid-conversation, handle the new topic fully while retaining all past context.
|
| 955 |
+
- Respond in the same language the user uses.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
"""
|
| 957 |
|
| 958 |
def build_tool_definitions_text(tools: List[dict]) -> str:
|
|
|
|
| 959 |
if not tools:
|
| 960 |
+
return "No tools currently available."
|
| 961 |
lines = []
|
| 962 |
for i, tool in enumerate(tools, 1):
|
| 963 |
+
fn = tool.get("function", tool) if tool.get("type") == "function" else tool
|
|
|
|
|
|
|
|
|
|
| 964 |
name = fn.get("name", f"tool_{i}")
|
| 965 |
desc = fn.get("description", "No description")
|
| 966 |
params = fn.get("parameters", {})
|
|
|
|
| 967 |
lines.append(f"### {i}. `{name}`")
|
| 968 |
lines.append(f"**Description:** {desc}")
|
|
|
|
| 969 |
if params and params.get("properties"):
|
| 970 |
lines.append("**Parameters:**")
|
| 971 |
+
for pname, pinfo in params["properties"].items():
|
| 972 |
+
req = pname in params.get("required", [])
|
| 973 |
ptype = pinfo.get("type", "any")
|
| 974 |
pdesc = pinfo.get("description", "")
|
| 975 |
+
lines.append(f" - `{pname}` ({ptype}){'*' if req else ''}: {pdesc}")
|
|
|
|
| 976 |
lines.append("")
|
| 977 |
return "\n".join(lines)
|
| 978 |
|
| 979 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 980 |
# โโโ Prompt Builder โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 981 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 982 |
|
| 983 |
+
def messages_to_prompt(
|
| 984 |
+
messages: list,
|
| 985 |
+
tools: list = None,
|
| 986 |
+
memory_context: str = "",
|
| 987 |
+
) -> str:
|
| 988 |
"""
|
| 989 |
+
ูุจูู prompt ูุงู
ู ุจุงูุชุฑุชูุจ:
|
| 990 |
+
1. ุฐุงูุฑุฉ Zep (ุงูุณูุงู ุงูุฏุงุฆู
ุนุจุฑ ุงูุฌูุณุงุช)
|
| 991 |
+
2. System prompt ุงูุฃุฏูุงุช (ุฅุฐุง ูุงูุช ููุงู ุฃุฏูุงุช)
|
| 992 |
+
3. ุฑุณุงุฆู ุงูู
ุญุงุฏุซุฉ ุงูุญุงููุฉ
|
| 993 |
"""
|
| 994 |
parts = []
|
| 995 |
|
| 996 |
+
# โโโ 1. ุงูุฐุงูุฑุฉ ุงูุฏุงุฆู
ุฉ ุฃููุงู (ุงูุฃุนูู ุฃููููุฉ)
|
| 997 |
+
if memory_context and memory_context.strip():
|
| 998 |
+
parts.append(memory_context)
|
| 999 |
+
|
| 1000 |
+
# โโโ 2. System prompt ููุฃุฏูุงุช
|
| 1001 |
if tools:
|
| 1002 |
+
tool_defs = build_tool_definitions_text(tools)
|
| 1003 |
+
sys_prompt = AGENT_SYSTEM_PROMPT.replace("{tool_definitions}", tool_defs)
|
| 1004 |
+
parts.append(f"[SYSTEM โ AGENT INSTRUCTIONS]\n{sys_prompt}")
|
| 1005 |
+
else:
|
| 1006 |
+
parts.append(
|
| 1007 |
+
"[SYSTEM]\nYou are a professional AI assistant with persistent memory. "
|
| 1008 |
+
"Use the memory context above to maintain continuity across all conversations."
|
| 1009 |
+
)
|
| 1010 |
|
| 1011 |
+
# โโโ 3. ู
ุนุงูุฌุฉ ุฑุณุงุฆู ุงูู
ุญุงุฏุซุฉ
|
| 1012 |
for msg in messages:
|
| 1013 |
role = msg.get("role", "user") if isinstance(msg, dict) else msg.role
|
| 1014 |
content = msg.get("content", "") if isinstance(msg, dict) else (msg.content or "")
|
| 1015 |
+
tc_data = msg.get("tool_calls") if isinstance(msg, dict) else getattr(msg, "tool_calls", None)
|
|
|
|
| 1016 |
|
| 1017 |
# ุชุญููู content ู
ุชุนุฏุฏ ุงูุฃุฌุฒุงุก
|
| 1018 |
if isinstance(content, list):
|
|
|
|
| 1020 |
c.get("text", "") for c in content
|
| 1021 |
if isinstance(c, dict) and c.get("type") in ("text", "input_text")
|
| 1022 |
)
|
| 1023 |
+
content = (content or "").strip()
|
| 1024 |
|
| 1025 |
if role == "system":
|
| 1026 |
+
# ุฏู
ุฌ system prompt ุงูู
ุณุชุฎุฏู
ู
ุน ู
ุง ุณุจู
|
| 1027 |
+
if content:
|
| 1028 |
+
parts.append(f"[ADDITIONAL SYSTEM INSTRUCTIONS]\n{content}")
|
| 1029 |
+
|
| 1030 |
+
elif role == "user":
|
| 1031 |
+
if content:
|
| 1032 |
+
parts.append(f"[USER]: {content}")
|
| 1033 |
|
| 1034 |
elif role == "assistant":
|
| 1035 |
+
if tc_data:
|
| 1036 |
+
tc_str = "\n".join(
|
| 1037 |
+
f"```tool_call\n{json.dumps({'name': tc['function']['name'], 'arguments': json.loads(tc['function'].get('arguments', '{}'))}, ensure_ascii=False)}\n```"
|
| 1038 |
+
for tc in tc_data
|
| 1039 |
+
)
|
| 1040 |
+
assistant_part = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
if content:
|
| 1042 |
+
assistant_part = f"[ASSISTANT]: {content}\n"
|
| 1043 |
+
assistant_part += f"[ASSISTANT called tools]:\n{tc_str}"
|
| 1044 |
+
parts.append(assistant_part)
|
| 1045 |
+
elif content:
|
| 1046 |
parts.append(f"[ASSISTANT]: {content}")
|
| 1047 |
|
| 1048 |
elif role == "tool":
|
| 1049 |
+
tool_name = msg.get("name", "") if isinstance(msg, dict) else getattr(msg, "name", "")
|
| 1050 |
+
tool_id = msg.get("tool_call_id", "") if isinstance(msg, dict) else getattr(msg, "tool_call_id", "")
|
| 1051 |
+
label = tool_name or tool_id or "unknown_tool"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
if content:
|
| 1053 |
+
parts.append(
|
| 1054 |
+
f"[TOOL RESULT โ {label}]:\n```json\n{content}\n```\n"
|
| 1055 |
+
f"โ Continue based on this result."
|
| 1056 |
+
)
|
| 1057 |
|
| 1058 |
+
return "\n\n".join(p for p in parts if p and p.strip())
|
| 1059 |
|
| 1060 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1061 |
+
# โโโ Agent Loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1062 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1063 |
|
| 1064 |
def run_agent_sync(
|
| 1065 |
messages: List[dict],
|
| 1066 |
model_id: int,
|
| 1067 |
think_mode: int,
|
| 1068 |
tools: Optional[List[dict]],
|
| 1069 |
+
memory_context: str = "",
|
| 1070 |
max_iterations: int = None,
|
| 1071 |
) -> Tuple[str, Optional[List[dict]], List[dict]]:
|
| 1072 |
"""
|
| 1073 |
+
Agent Loop (ReAct):
|
| 1074 |
+
Reason โ Act (tool call) โ Observe (result) โ Reason โ ...
|
| 1075 |
+
|
| 1076 |
+
ูุนูุฏ (final_text, tool_calls_if_any, updated_messages)
|
|
|
|
| 1077 |
|
| 1078 |
+
ู
ูุงุญุธุฉ: ูู ูุถุน non-executor (ุงูุงูุชุฑุงุถู)ุ ููุนูุฏ tool calls ููู client
|
| 1079 |
+
ููููููุฐูุง ูููุฑุณู ุงููุชุงุฆุฌ ูู ุงูุทูุจ ุงูุชุงูู. ูุฐุง ูู ุงูุณููู ุงูุตุญูุญ
|
| 1080 |
+
ูุจุฑูุชูููู OpenAI Tool Calling.
|
| 1081 |
"""
|
| 1082 |
if max_iterations is None:
|
| 1083 |
max_iterations = CONFIG["agent_max_iterations"]
|
| 1084 |
|
| 1085 |
current_messages = list(messages)
|
|
|
|
|
|
|
| 1086 |
|
| 1087 |
+
for iteration in range(1, max_iterations + 1):
|
|
|
|
| 1088 |
log(f"Agent iteration {iteration}/{max_iterations}", "AGENT")
|
| 1089 |
|
| 1090 |
+
# ุจูุงุก ุงูู prompt ู
ุน ุงูุฐุงูุฑุฉ
|
| 1091 |
+
prompt = messages_to_prompt(current_messages, tools, memory_context)
|
| 1092 |
+
|
| 1093 |
+
try:
|
| 1094 |
+
raw = gemini_stream_generate(prompt, model_id, think_mode)
|
| 1095 |
+
text = extract_response_text(raw)
|
| 1096 |
+
except Exception as e:
|
| 1097 |
+
log(f"Gemini call error (iter {iteration}): {e}", "ERROR")
|
| 1098 |
+
return f"[Error communicating with model: {e}]", None, current_messages
|
| 1099 |
|
| 1100 |
if not text:
|
| 1101 |
log("Empty response from Gemini", "WARN")
|
| 1102 |
+
if iteration == 1:
|
| 1103 |
+
return "[No response from model]", None, current_messages
|
| 1104 |
break
|
| 1105 |
|
| 1106 |
+
# ุชุญููู tool calls
|
| 1107 |
clean_text, tool_calls = parse_tool_calls(text)
|
| 1108 |
|
| 1109 |
if tool_calls and tools:
|
| 1110 |
+
names = [tc["function"]["name"] for tc in tool_calls]
|
| 1111 |
+
log(f"Tool calls detected: {names}", "TOOL")
|
| 1112 |
|
| 1113 |
+
# ุฃุถู ุฑุฏ ุงูู
ุณุงุนุฏ ููุณูุงู
|
| 1114 |
current_messages.append({
|
| 1115 |
"role": "assistant",
|
| 1116 |
"content": clean_text or None,
|
| 1117 |
"tool_calls": tool_calls,
|
| 1118 |
})
|
|
|
|
| 1119 |
|
| 1120 |
+
# ุฃุนุฏ tool calls ููู client (OpenAI protocol)
|
| 1121 |
+
return clean_text or "", tool_calls, current_messages
|
|
|
|
|
|
|
| 1122 |
|
| 1123 |
else:
|
| 1124 |
+
# ุฑุฏ ููุงุฆู
|
| 1125 |
+
log(f"โ
Final answer at iteration {iteration}", "AGENT")
|
| 1126 |
return clean_text or text, None, current_messages
|
| 1127 |
|
| 1128 |
log(f"Max iterations ({max_iterations}) reached", "WARN")
|
| 1129 |
+
return text or "[Max iterations reached]", None, current_messages
|
| 1130 |
|
| 1131 |
|
| 1132 |
+
def run_agent_with_executor_sync(
|
| 1133 |
messages: List[dict],
|
| 1134 |
model_id: int,
|
| 1135 |
think_mode: int,
|
| 1136 |
tools: Optional[List[dict]],
|
| 1137 |
+
tool_executor: Optional[Callable[[str, dict], str]],
|
| 1138 |
+
memory_context: str = "",
|
| 1139 |
max_iterations: int = None,
|
| 1140 |
) -> Tuple[str, Optional[List[dict]], List[dict]]:
|
| 1141 |
"""
|
| 1142 |
+
Agent Loop ุงููุงู
ู ู
ุน ุชูููุฐ ุงูุฃุฏูุงุช ุฏุงุฎููุงู.
|
| 1143 |
+
ูุณุชุฎุฏู
ุนูุฏู
ุง ูููู tool_executor ู
ุชุงุญุงู (ูุงุฏุฑ ูู ูุฐุง ุงูุณููุงุฑูู).
|
|
|
|
| 1144 |
"""
|
| 1145 |
if max_iterations is None:
|
| 1146 |
max_iterations = CONFIG["agent_max_iterations"]
|
|
|
|
| 1148 |
current_messages = list(messages)
|
| 1149 |
|
| 1150 |
for iteration in range(1, max_iterations + 1):
|
| 1151 |
+
log(f"[Full Agent] Iteration {iteration}/{max_iterations}", "AGENT")
|
| 1152 |
|
| 1153 |
+
prompt = messages_to_prompt(current_messages, tools, memory_context)
|
| 1154 |
try:
|
| 1155 |
raw = gemini_stream_generate(prompt, model_id, think_mode)
|
| 1156 |
text = extract_response_text(raw)
|
| 1157 |
except Exception as e:
|
| 1158 |
+
log(f"Gemini error: {e}", "ERROR")
|
| 1159 |
return f"Error: {e}", None, current_messages
|
| 1160 |
|
| 1161 |
if not text:
|
|
|
|
| 1162 |
break
|
| 1163 |
|
| 1164 |
clean_text, tool_calls = parse_tool_calls(text)
|
| 1165 |
|
| 1166 |
if not tool_calls or not tools:
|
| 1167 |
+
log(f"โ
Final answer at iteration {iteration}", "AGENT")
|
|
|
|
| 1168 |
return clean_text or text, None, current_messages
|
| 1169 |
|
|
|
|
| 1170 |
current_messages.append({
|
| 1171 |
"role": "assistant",
|
| 1172 |
"content": clean_text or None,
|
| 1173 |
"tool_calls": tool_calls,
|
| 1174 |
})
|
| 1175 |
|
|
|
|
| 1176 |
for tc in tool_calls:
|
| 1177 |
fn_name = tc["function"]["name"]
|
| 1178 |
try:
|
|
|
|
| 1180 |
except Exception:
|
| 1181 |
fn_args = {}
|
| 1182 |
|
| 1183 |
+
log(f"Executing: {fn_name}({fn_args})", "TOOL")
|
| 1184 |
|
| 1185 |
if tool_executor:
|
| 1186 |
try:
|
|
|
|
| 1188 |
result_str = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
|
| 1189 |
except Exception as e:
|
| 1190 |
result_str = json.dumps({"error": str(e)})
|
| 1191 |
+
log(f"Tool error: {e}", "ERROR")
|
| 1192 |
else:
|
| 1193 |
+
result_str = json.dumps({"status": "no_executor", "tool": fn_name})
|
|
|
|
| 1194 |
|
| 1195 |
current_messages.append({
|
| 1196 |
"role": "tool",
|
|
|
|
| 1199 |
"content": result_str,
|
| 1200 |
})
|
| 1201 |
|
|
|
|
|
|
|
|
|
|
| 1202 |
return text or "", None, current_messages
|
| 1203 |
|
| 1204 |
|
|
|
|
|
|
|
| 1205 |
def call_gemini_sync(
|
| 1206 |
+
prompt: str,
|
| 1207 |
+
model_id: int,
|
| 1208 |
+
think_mode: int,
|
| 1209 |
+
tools: list = None,
|
| 1210 |
) -> Tuple[str, Optional[list]]:
|
| 1211 |
+
"""ุงุณุชุฏุนุงุก ุจุณูุท ุจุฏูู agent loop."""
|
| 1212 |
raw = gemini_stream_generate(prompt, model_id, think_mode)
|
| 1213 |
text = extract_response_text(raw)
|
| 1214 |
tool_calls = None
|
|
|
|
| 1216 |
text, tool_calls = parse_tool_calls(text)
|
| 1217 |
return text or "", tool_calls
|
| 1218 |
|
| 1219 |
+
# โโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1220 |
# โโโ Model Resolution โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1221 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1222 |
|
| 1223 |
def resolve_model(model_name: str) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[str]]:
|
| 1224 |
think_override = None
|
|
|
|
| 1228 |
think_override = int(think_str)
|
| 1229 |
except ValueError:
|
| 1230 |
pass
|
| 1231 |
+
|
| 1232 |
cfg = MODELS.get(model_name)
|
| 1233 |
if not cfg:
|
|
|
|
| 1234 |
default = CONFIG.get("default_model", "gemini-3.5-flash")
|
| 1235 |
cfg = MODELS.get(default)
|
| 1236 |
if cfg:
|
|
|
|
| 1238 |
model_name = default
|
| 1239 |
else:
|
| 1240 |
return None, None, None, f"Unknown model: {model_name}. Available: {list(MODELS.keys())}"
|
| 1241 |
+
|
| 1242 |
return model_name, cfg["mode"], (think_override if think_override is not None else cfg["think"]), None
|
| 1243 |
|
| 1244 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1245 |
+
# โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1246 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1247 |
+
|
| 1248 |
+
def extract_last_user_query(messages: List[dict]) -> str:
|
| 1249 |
+
"""ูุณุชุฎุฑุฌ ุขุฎุฑ ุฑุณุงูุฉ ููู
ุณุชุฎุฏู
ููุต ููุจุญุซ ุงูุฏูุงูู."""
|
| 1250 |
+
for msg in reversed(messages):
|
| 1251 |
+
if msg.get("role") == "user":
|
| 1252 |
+
content = msg.get("content", "")
|
| 1253 |
+
if isinstance(content, list):
|
| 1254 |
+
content = " ".join(
|
| 1255 |
+
c.get("text", "") for c in content
|
| 1256 |
+
if isinstance(c, dict) and c.get("type") in ("text", "input_text")
|
| 1257 |
+
)
|
| 1258 |
+
return (content or "").strip()
|
| 1259 |
+
return ""
|
| 1260 |
+
|
| 1261 |
+
|
| 1262 |
+
def _build_completion_response(
|
| 1263 |
+
cid: str, created: int, model: str, text: str,
|
| 1264 |
+
tool_calls: Optional[list], prompt: str = ""
|
| 1265 |
+
) -> dict:
|
| 1266 |
+
msg: dict = {"role": "assistant", "content": text or None}
|
| 1267 |
+
if tool_calls:
|
| 1268 |
+
msg["tool_calls"] = tool_calls
|
| 1269 |
+
finish = "tool_calls" if tool_calls else "stop"
|
| 1270 |
+
return {
|
| 1271 |
+
"id": cid,
|
| 1272 |
+
"object": "chat.completion",
|
| 1273 |
+
"created": created,
|
| 1274 |
+
"model": model,
|
| 1275 |
+
"choices": [{"index": 0, "message": msg, "finish_reason": finish}],
|
| 1276 |
+
"usage": {
|
| 1277 |
+
"prompt_tokens": len(prompt) // 4,
|
| 1278 |
+
"completion_tokens": len(text) // 4 if text else 0,
|
| 1279 |
+
"total_tokens": (len(prompt) + len(text)) // 4 if text else len(prompt) // 4,
|
| 1280 |
+
},
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1284 |
# โโโ Authentication โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1285 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1286 |
|
| 1287 |
security = HTTPBearer(auto_error=False)
|
| 1288 |
|
| 1289 |
async def verify_api_key(
|
| 1290 |
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
| 1291 |
) -> str:
|
| 1292 |
api_keys = CONFIG.get("api_keys", [])
|
| 1293 |
if not api_keys:
|
|
|
|
| 1299 |
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 1300 |
return token
|
| 1301 |
|
| 1302 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1303 |
# โโโ FastAPI App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1304 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1305 |
|
| 1306 |
@asynccontextmanager
|
| 1307 |
async def lifespan(app: FastAPI):
|
| 1308 |
await services.init_db()
|
| 1309 |
await services.start_cleanup()
|
| 1310 |
+
await zep_memory.init() # โ ุชููุฆุฉ Zep Cloud
|
| 1311 |
log(f"gemini-web2api v{__version__} started", "INFO")
|
| 1312 |
+
log(f"Memory: {zep_memory.status()}", "MEM")
|
| 1313 |
yield
|
| 1314 |
if services._cleanup_task:
|
| 1315 |
services._cleanup_task.cancel()
|
| 1316 |
|
| 1317 |
app = FastAPI(
|
| 1318 |
+
title="Gemini Web2API + Zep Memory (Agent Edition)",
|
| 1319 |
version=__version__,
|
| 1320 |
+
description="OpenAI-compatible Gemini proxy with persistent Zep Cloud memory and full Agent Loop",
|
| 1321 |
lifespan=lifespan,
|
| 1322 |
)
|
| 1323 |
|
|
|
|
| 1329 |
allow_headers=["*"],
|
| 1330 |
)
|
| 1331 |
|
| 1332 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1333 |
# โโโ /v1/models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1334 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1335 |
|
| 1336 |
@app.get("/v1/models")
|
| 1337 |
async def list_models():
|
|
|
|
| 1348 |
"tool_use": True,
|
| 1349 |
"streaming": True,
|
| 1350 |
"agent_loop": True,
|
| 1351 |
+
"persistent_memory": zep_memory.enabled,
|
| 1352 |
},
|
| 1353 |
}
|
| 1354 |
for name, cfg in MODELS.items()
|
| 1355 |
],
|
| 1356 |
}
|
| 1357 |
|
|
|
|
|
|
|
| 1358 |
@app.get("/v1beta/models")
|
| 1359 |
async def google_models_list():
|
| 1360 |
return {
|
|
|
|
| 1369 |
]
|
| 1370 |
}
|
| 1371 |
|
| 1372 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1373 |
# โโโ /v1/chat/completions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1374 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1375 |
|
| 1376 |
@app.post("/v1/chat/completions")
|
| 1377 |
async def chat_completions(
|
|
|
|
| 1379 |
request: Request,
|
| 1380 |
_: str = Depends(verify_api_key),
|
| 1381 |
):
|
| 1382 |
+
# โโโ Rate Limiting
|
| 1383 |
client_ip = request.client.host
|
| 1384 |
allowed, wait_time = await services.check_rate_limit(client_ip)
|
| 1385 |
if not allowed:
|
|
|
|
| 1389 |
headers={"Retry-After": str(wait_time)},
|
| 1390 |
)
|
| 1391 |
|
| 1392 |
+
# โโโ Model Resolution
|
| 1393 |
model_name, model_id, think_mode, err = resolve_model(
|
| 1394 |
body.model or CONFIG["default_model"]
|
| 1395 |
)
|
|
|
|
| 1398 |
|
| 1399 |
tools = body.tools
|
| 1400 |
messages = [m.dict(exclude_none=True) for m in body.messages]
|
| 1401 |
+
# user_id: ูุณุชุฎุฏู
ู ูู
ูุชุงุญ ููุฐุงูุฑุฉ โ ุซุงุจุช ููู ู
ุณุชุฎุฏู
|
| 1402 |
+
user_id = body.user or client_ip or "default"
|
| 1403 |
+
|
| 1404 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1405 |
+
# โ
ZEP: ุชุฌููุฒ ุงูุฌูุณุฉ ูุงุณุชุฑุฌุงุน ุงูุฐุงูุฑุฉ ุงููุงู
ูุฉ
|
| 1406 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1407 |
+
await zep_memory.ensure_user_session(user_id)
|
| 1408 |
+
|
| 1409 |
+
last_query = extract_last_user_query(messages)
|
| 1410 |
+
|
| 1411 |
+
# ุงุณุชุฑุฌุงุน ุงูุฐุงูุฑุฉ ูุงูุจุญุซ ุงูุฏูุงูู ุจุงูุชูุงุฒู
|
| 1412 |
+
memory_context = await zep_memory.get_full_context(user_id, last_query)
|
| 1413 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1414 |
+
|
| 1415 |
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
|
| 1416 |
created = int(time.time())
|
| 1417 |
|
| 1418 |
+
# Cache key (ุจุฏูู ุฐุงูุฑุฉ ูุฃู ุงูุฐุงูุฑุฉ ุชุชุบูุฑ)
|
|
|
|
| 1419 |
cache_key = hashlib.md5(
|
| 1420 |
+
f"{model_name}:{user_id}:{last_query[:200]}:{bool(tools)}".encode()
|
| 1421 |
).hexdigest()
|
| 1422 |
|
| 1423 |
+
# โโโ Streaming โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1424 |
if body.stream:
|
| 1425 |
async def stream_gen():
|
| 1426 |
full_text = ""
|
| 1427 |
+
final_tool_calls = None
|
| 1428 |
try:
|
| 1429 |
if tools:
|
|
|
|
| 1430 |
loop = asyncio.get_event_loop()
|
| 1431 |
+
text, tc, updated_msgs = await loop.run_in_executor(
|
| 1432 |
None, run_agent_sync,
|
| 1433 |
messages, model_id, think_mode, tools,
|
| 1434 |
+
memory_context, CONFIG["agent_max_iterations"]
|
| 1435 |
)
|
| 1436 |
full_text = text or ""
|
| 1437 |
+
final_tool_calls = tc
|
| 1438 |
|
| 1439 |
+
if tc:
|
| 1440 |
# ุฅุฑุณุงู tool calls chunk
|
|
|
|
| 1441 |
chunk = {
|
| 1442 |
"id": cid, "object": "chat.completion.chunk",
|
| 1443 |
"created": created, "model": model_name,
|
| 1444 |
+
"choices": [{
|
| 1445 |
+
"index": 0,
|
| 1446 |
+
"delta": {"role": "assistant", "content": full_text or None, "tool_calls": tc},
|
| 1447 |
+
"finish_reason": "tool_calls",
|
| 1448 |
+
}],
|
| 1449 |
}
|
| 1450 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1451 |
else:
|
|
|
|
| 1452 |
if full_text:
|
| 1453 |
chunk = {
|
| 1454 |
"id": cid, "object": "chat.completion.chunk",
|
| 1455 |
"created": created, "model": model_name,
|
| 1456 |
+
"choices": [{
|
| 1457 |
+
"index": 0,
|
| 1458 |
+
"delta": {"role": "assistant", "content": full_text},
|
| 1459 |
+
"finish_reason": None,
|
| 1460 |
+
}],
|
| 1461 |
}
|
| 1462 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1463 |
|
| 1464 |
else:
|
| 1465 |
# Streaming ุจุฏูู ุฃุฏูุงุช
|
| 1466 |
+
prompt = messages_to_prompt(messages, memory_context=memory_context)
|
| 1467 |
for delta in gemini_stream_iter(prompt, model_id, think_mode):
|
| 1468 |
full_text += delta
|
| 1469 |
chunk = {
|
| 1470 |
"id": cid, "object": "chat.completion.chunk",
|
| 1471 |
"created": created, "model": model_name,
|
| 1472 |
+
"choices": [{
|
| 1473 |
+
"index": 0,
|
| 1474 |
+
"delta": {"content": delta},
|
| 1475 |
+
"finish_reason": None,
|
| 1476 |
+
}],
|
| 1477 |
}
|
| 1478 |
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
| 1479 |
|
| 1480 |
# Final chunk
|
| 1481 |
+
finish = "tool_calls" if final_tool_calls else "stop"
|
| 1482 |
+
yield f"data: {json.dumps({'id': cid, 'object': 'chat.completion.chunk', 'created': created, 'model': model_name, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': finish}]})}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1483 |
yield "data: [DONE]\n\n"
|
| 1484 |
|
| 1485 |
+
# โ
ZEP: ุญูุธ ุงูู
ุญุงุฏุซุฉ ุจุนุฏ ุงูุงูุชูุงุก
|
| 1486 |
+
if full_text and not final_tool_calls:
|
| 1487 |
+
save_msgs = [m for m in messages if m.get("role") in ("user", "assistant", "tool")]
|
| 1488 |
+
await zep_memory.save_turn(user_id, save_msgs, full_text)
|
| 1489 |
|
| 1490 |
except Exception as e:
|
| 1491 |
log(f"Stream error: {e}", "ERROR")
|
|
|
|
| 1493 |
|
| 1494 |
return StreamingResponse(stream_gen(), media_type="text/event-stream")
|
| 1495 |
|
| 1496 |
+
# โโโ Non-streaming โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1497 |
+
# Cache (ุจุฏูู ุฃุฏูุงุช ููุท)
|
| 1498 |
if not tools:
|
| 1499 |
cached = await services.get_cache(cache_key)
|
| 1500 |
if cached:
|
|
|
|
| 1503 |
try:
|
| 1504 |
loop = asyncio.get_event_loop()
|
| 1505 |
if tools:
|
| 1506 |
+
text, tool_calls, updated_msgs = await loop.run_in_executor(
|
|
|
|
| 1507 |
None, run_agent_sync,
|
| 1508 |
messages, model_id, think_mode, tools,
|
| 1509 |
+
memory_context, CONFIG["agent_max_iterations"]
|
| 1510 |
)
|
| 1511 |
else:
|
| 1512 |
+
prompt = messages_to_prompt(messages, memory_context=memory_context)
|
| 1513 |
text, tool_calls = await loop.run_in_executor(
|
| 1514 |
+
None, call_gemini_sync,
|
| 1515 |
+
prompt, model_id, think_mode, None
|
| 1516 |
)
|
| 1517 |
except Exception as e:
|
| 1518 |
log(f"Error: {e}", "ERROR")
|
| 1519 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1520 |
|
| 1521 |
+
# โ
ZEP: ุญูุธ ุงูู
ุญุงุฏุซุฉ
|
| 1522 |
+
if text and not tool_calls:
|
| 1523 |
+
save_msgs = [m for m in messages if m.get("role") in ("user", "assistant", "tool")]
|
| 1524 |
+
await zep_memory.save_turn(user_id, save_msgs, text)
|
| 1525 |
+
|
| 1526 |
prompt_text = messages_to_prompt(messages)
|
| 1527 |
resp_data = _build_completion_response(cid, created, model_name, text, tool_calls, prompt_text)
|
| 1528 |
|
| 1529 |
+
if not tools and text:
|
| 1530 |
await services.set_cache(cache_key, json.dumps(resp_data))
|
| 1531 |
|
| 1532 |
return JSONResponse(content=resp_data)
|
| 1533 |
|
| 1534 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1535 |
# โโโ /v1/responses (Codex CLI / Responses API) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1536 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1537 |
|
| 1538 |
@app.post("/v1/responses")
|
| 1539 |
async def responses_api(
|
|
|
|
| 1550 |
input_items = req.get("input", [])
|
| 1551 |
tools = req.get("tools")
|
| 1552 |
messages = []
|
| 1553 |
+
user_id = req.get("user", "default")
|
| 1554 |
|
| 1555 |
if req.get("instructions"):
|
| 1556 |
messages.append({"role": "system", "content": req["instructions"]})
|
|
|
|
| 1622 |
if not any(m.get("content") for m in messages):
|
| 1623 |
raise HTTPException(status_code=400, detail="Empty input")
|
| 1624 |
|
| 1625 |
+
# โ
Zep Memory
|
| 1626 |
+
await zep_memory.ensure_user_session(user_id)
|
| 1627 |
+
last_query = extract_last_user_query(messages)
|
| 1628 |
+
memory_context = await zep_memory.get_full_context(user_id, last_query)
|
| 1629 |
+
|
| 1630 |
loop = asyncio.get_event_loop()
|
| 1631 |
try:
|
| 1632 |
if tools:
|
| 1633 |
text, tool_calls, _ = await loop.run_in_executor(
|
| 1634 |
+
None, run_agent_sync,
|
| 1635 |
+
messages, model_id, think_mode, tools,
|
| 1636 |
+
memory_context, CONFIG["agent_max_iterations"]
|
| 1637 |
)
|
| 1638 |
else:
|
| 1639 |
+
prompt = messages_to_prompt(messages, memory_context=memory_context)
|
| 1640 |
text, tool_calls = await loop.run_in_executor(
|
| 1641 |
None, call_gemini_sync, prompt, model_id, think_mode, None
|
| 1642 |
)
|
| 1643 |
except Exception as e:
|
| 1644 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1645 |
|
| 1646 |
+
# โ
ุญูุธ ูู Zep
|
| 1647 |
+
if text and not tool_calls:
|
| 1648 |
+
save_msgs = [m for m in messages if m.get("role") in ("user", "assistant", "tool")]
|
| 1649 |
+
await zep_memory.save_turn(user_id, save_msgs, text)
|
| 1650 |
+
|
| 1651 |
rid = f"resp_{uuid.uuid4().hex[:16]}"
|
| 1652 |
mid = f"msg_{uuid.uuid4().hex[:12]}"
|
| 1653 |
output = []
|
|
|
|
| 1672 |
})
|
| 1673 |
|
| 1674 |
prompt_text = messages_to_prompt(messages)
|
| 1675 |
+
|
| 1676 |
if req.get("stream"):
|
| 1677 |
async def responses_stream():
|
| 1678 |
yield f"event: response.created\ndata: {json.dumps({'type': 'response.created', 'response': {'id': rid, 'object': 'response', 'status': 'in_progress', 'model': model_name, 'output': []}})}\n\n"
|
|
|
|
| 1705 |
},
|
| 1706 |
})
|
| 1707 |
|
| 1708 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1709 |
# โโโ Google Native API /v1beta โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1710 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1711 |
|
| 1712 |
+
def _google_contents_to_messages(req: dict) -> list:
|
| 1713 |
+
messages = []
|
| 1714 |
sys_inst = req.get("systemInstruction")
|
| 1715 |
if sys_inst:
|
| 1716 |
sys_text = " ".join(p.get("text", "") for p in sys_inst.get("parts", []) if p.get("text"))
|
| 1717 |
if sys_text:
|
| 1718 |
+
messages.append({"role": "system", "content": sys_text})
|
| 1719 |
for content in req.get("contents", []):
|
| 1720 |
+
role = "assistant" if content.get("role") == "model" else "user"
|
| 1721 |
text = " ".join(p.get("text", "") for p in content.get("parts", []) if p.get("text"))
|
| 1722 |
+
messages.append({"role": role, "content": text})
|
| 1723 |
+
return messages
|
|
|
|
|
|
|
|
|
|
| 1724 |
|
| 1725 |
@app.post("/v1beta/models/{model_path:path}")
|
| 1726 |
async def google_generate(model_path: str, request: Request):
|
| 1727 |
req = await request.json()
|
| 1728 |
model_name_raw = model_path.split(":")[0]
|
| 1729 |
+
is_stream = ":streamGenerateContent" in model_path
|
| 1730 |
|
| 1731 |
model_name, model_id, think_mode, err = resolve_model(model_name_raw)
|
| 1732 |
if err:
|
| 1733 |
raise HTTPException(status_code=400, detail=err)
|
| 1734 |
|
| 1735 |
+
messages = _google_contents_to_messages(req)
|
| 1736 |
+
if not messages:
|
| 1737 |
raise HTTPException(status_code=400, detail="Empty content")
|
| 1738 |
|
| 1739 |
+
# Zep ููู Google API ุฃูุถุงู
|
| 1740 |
+
user_id = req.get("user", "google_api")
|
| 1741 |
+
await zep_memory.ensure_user_session(user_id)
|
| 1742 |
+
last_query = extract_last_user_query(messages)
|
| 1743 |
+
memory_context = await zep_memory.get_full_context(user_id, last_query)
|
| 1744 |
+
|
| 1745 |
+
prompt = messages_to_prompt(messages, memory_context=memory_context)
|
| 1746 |
loop = asyncio.get_event_loop()
|
| 1747 |
try:
|
| 1748 |
text, _ = await loop.run_in_executor(
|
|
|
|
| 1751 |
except Exception as e:
|
| 1752 |
raise HTTPException(status_code=502, detail=f"Upstream error: {e}")
|
| 1753 |
|
| 1754 |
+
if text:
|
| 1755 |
+
await zep_memory.save_turn(user_id, messages, text)
|
| 1756 |
+
|
| 1757 |
candidate = {
|
| 1758 |
"content": {"parts": [{"text": text or ""}], "role": "model"},
|
| 1759 |
"finishReason": "STOP",
|
|
|
|
| 1769 |
"modelVersion": model_name,
|
| 1770 |
}
|
| 1771 |
|
| 1772 |
+
if is_stream:
|
| 1773 |
async def google_stream():
|
| 1774 |
yield f"data: {json.dumps(response_obj)}\n\n"
|
| 1775 |
return StreamingResponse(google_stream(), media_type="text/event-stream")
|
| 1776 |
|
| 1777 |
return JSONResponse(response_obj)
|
| 1778 |
|
| 1779 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1780 |
# โโโ Health & Root โโโโโโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1781 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1782 |
|
| 1783 |
@app.get("/")
|
| 1784 |
async def root():
|
|
|
|
| 1786 |
"service": "gemini-web2api",
|
| 1787 |
"version": __version__,
|
| 1788 |
"models": list(MODELS.keys()),
|
| 1789 |
+
"memory": zep_memory.status(),
|
| 1790 |
"features": {
|
| 1791 |
+
"persistent_memory": zep_memory.enabled,
|
| 1792 |
"agent_loop": True,
|
| 1793 |
"tool_calling": True,
|
| 1794 |
"streaming": True,
|
| 1795 |
"cache": True,
|
| 1796 |
"rate_limiting": True,
|
| 1797 |
},
|
| 1798 |
+
"endpoints": [
|
| 1799 |
+
"/v1/chat/completions",
|
| 1800 |
+
"/v1/models",
|
| 1801 |
+
"/v1/responses",
|
| 1802 |
+
"/v1beta/models",
|
| 1803 |
+
"/health",
|
| 1804 |
+
"/memory/status",
|
| 1805 |
+
],
|
| 1806 |
}
|
| 1807 |
|
| 1808 |
@app.get("/health")
|
| 1809 |
async def health():
|
| 1810 |
+
return {
|
| 1811 |
+
"status": "healthy",
|
| 1812 |
+
"timestamp": time.time(),
|
| 1813 |
+
"version": __version__,
|
| 1814 |
+
"memory_enabled": zep_memory.enabled,
|
| 1815 |
+
}
|
| 1816 |
|
| 1817 |
+
@app.get("/memory/status")
|
| 1818 |
+
async def memory_status():
|
| 1819 |
+
"""ุญุงูุฉ ุงูุฐุงูุฑุฉ ุงูุฏุงุฆู
ุฉ."""
|
| 1820 |
+
return {
|
| 1821 |
+
"provider": "Zep Cloud",
|
| 1822 |
+
"enabled": zep_memory.enabled,
|
| 1823 |
+
"config": {
|
| 1824 |
+
"memory_type": CONFIG.get("zep_memory_type"),
|
| 1825 |
+
"recent_messages": CONFIG.get("zep_recent_messages"),
|
| 1826 |
+
"search_limit": CONFIG.get("zep_search_limit"),
|
| 1827 |
+
"min_fact_rating": CONFIG.get("zep_min_fact_rating"),
|
| 1828 |
+
},
|
| 1829 |
+
"error": zep_memory._init_error if not zep_memory.enabled else None,
|
| 1830 |
+
}
|
| 1831 |
+
|
| 1832 |
+
@app.post("/memory/add-fact")
|
| 1833 |
+
async def add_fact_endpoint(
|
| 1834 |
+
request: Request,
|
| 1835 |
+
_: str = Depends(verify_api_key),
|
| 1836 |
+
):
|
| 1837 |
+
"""ุฅุถุงูุฉ ุญูููุฉ ุตุฑูุญุฉ ูุฐุงูุฑุฉ ู
ุณุชุฎุฏู
ู
ุญุฏุฏ."""
|
| 1838 |
+
body = await request.json()
|
| 1839 |
+
user_id = body.get("user_id", "default")
|
| 1840 |
+
fact = body.get("fact", "")
|
| 1841 |
+
if not fact:
|
| 1842 |
+
raise HTTPException(status_code=400, detail="fact is required")
|
| 1843 |
+
|
| 1844 |
+
await zep_memory.ensure_user_session(user_id)
|
| 1845 |
+
await zep_memory.add_fact(user_id, fact)
|
| 1846 |
+
return {"status": "ok", "fact": fact, "user_id": user_id}
|
| 1847 |
+
|
| 1848 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1849 |
# โโโ Gradio UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1850 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1851 |
|
| 1852 |
def build_gradio_interface():
|
| 1853 |
+
async def chat_handler(
|
| 1854 |
+
message: str,
|
| 1855 |
+
history: list,
|
| 1856 |
+
model_choice: str,
|
| 1857 |
+
user_id_box: str,
|
| 1858 |
+
request: gr.Request,
|
| 1859 |
+
):
|
| 1860 |
if not message or not message.strip():
|
| 1861 |
yield history
|
| 1862 |
return
|
|
|
|
| 1867 |
yield history
|
| 1868 |
return
|
| 1869 |
|
| 1870 |
+
uid = (user_id_box or "").strip() or (request.client.host if request.client else "default")
|
| 1871 |
+
|
| 1872 |
+
# ุชุฌููุฒ ุงูุฌูุณุฉ ูุงุณุชุฑุฌุงุน ุงูุฐุงูุฑุฉ
|
| 1873 |
+
await zep_memory.ensure_user_session(uid)
|
| 1874 |
+
memory_context = await zep_memory.get_full_context(uid, message)
|
| 1875 |
+
|
| 1876 |
+
history.append((message, "โณ ..."))
|
| 1877 |
yield history
|
| 1878 |
|
| 1879 |
full_response = ""
|
| 1880 |
try:
|
| 1881 |
+
msgs = [{"role": "user", "content": message}]
|
| 1882 |
+
prompt = messages_to_prompt(msgs, memory_context=memory_context)
|
| 1883 |
+
|
| 1884 |
for chunk in gemini_stream_iter(prompt, model_id, think_mode):
|
| 1885 |
full_response += chunk
|
| 1886 |
history[-1] = (message, full_response)
|
| 1887 |
yield history
|
| 1888 |
+
|
| 1889 |
except Exception as e:
|
| 1890 |
full_response = f"โ Error: {e}"
|
| 1891 |
history[-1] = (message, full_response)
|
| 1892 |
yield history
|
| 1893 |
+
return
|
| 1894 |
|
| 1895 |
+
# ุญูุธ ูู Zep
|
| 1896 |
+
if full_response:
|
| 1897 |
+
await zep_memory.save_turn(
|
| 1898 |
+
uid,
|
| 1899 |
+
[{"role": "user", "content": message}],
|
| 1900 |
+
full_response,
|
| 1901 |
+
)
|
| 1902 |
+
log(f"Saved chat turn to Zep for user={uid}", "MEM")
|
| 1903 |
+
|
| 1904 |
+
mem_status = "โ
Zep Cloud ู
ุชุตู" if HAS_ZEP else "โ zep-cloud ุบูุฑ ู
ุซุจูุช"
|
| 1905 |
api_key_display = CONFIG.get("api_keys", ["sk-gemini"])[0]
|
| 1906 |
|
| 1907 |
+
with gr.Blocks(
|
| 1908 |
+
title="Gemini Agent + Zep Memory",
|
| 1909 |
+
theme=gr.themes.Soft(),
|
| 1910 |
+
) as demo:
|
| 1911 |
gr.Markdown(f"""
|
| 1912 |
+
# ๐ค Gemini Professional Agent + ๐ง Zep Persistent Memory
|
| 1913 |
+
### v{__version__} โ ุฐุงูุฑุฉ ุฏุงุฆู
ุฉ ุนุจุฑ ูู ุงูุฌูุณุงุช
|
| 1914 |
+
|
| 1915 |
+
| ุงูุฎุงุตูุฉ | ุงูููู
ุฉ |
|
| 1916 |
+
|---------|--------|
|
| 1917 |
+
| **Memory** | {mem_status} |
|
| 1918 |
+
| **Agent Loop** | โ
ุญุชู {CONFIG['agent_max_iterations']} iterations |
|
| 1919 |
+
| **API Key** | `{api_key_display}` |
|
| 1920 |
+
| **Base URL** | `/v1` |
|
| 1921 |
""")
|
| 1922 |
|
| 1923 |
with gr.Row():
|
| 1924 |
with gr.Column(scale=4):
|
| 1925 |
+
chatbot = gr.Chatbot(
|
| 1926 |
+
height=520,
|
| 1927 |
+
label="ุงูู
ุญุงุฏุซุฉ โ ุชูุญูุธ ุชููุงุฆูุงู ูู Zep",
|
| 1928 |
+
show_copy_button=True,
|
| 1929 |
+
)
|
| 1930 |
with gr.Column(scale=1):
|
| 1931 |
+
gr.Markdown("""
|
| 1932 |
+
### ๐ง ุงูุฐุงูุฑุฉ ุงูุฏุงุฆู
ุฉ
|
| 1933 |
+
ูุชุฐูุฑ ุงููููู ุนุจุฑ **ูู ุงูุฌูุณุงุช**:
|
| 1934 |
+
- ุงูุญูุงุฆู ุงูู
ุณุชุฎุฑุฌุฉ ุชููุงุฆูุงู
|
| 1935 |
+
- ู
ูุฎุต ูุงู
ู ููู
ุญุงุฏุซุงุช
|
| 1936 |
+
- ุขุฎุฑ 20 ุฑุณุงูุฉ ุจุงูุชูุตูู
|
| 1937 |
+
- ุจุญุซ ุฏูุงูู ูู ูู ุงูุชุงุฑูุฎ
|
| 1938 |
+
|
| 1939 |
+
### โ
ุงูู
ู
ูุฒุงุช
|
| 1940 |
+
- ูุชุฐูุฑ ุชุบููุฑุงุช ุงูู
ูุถูุน
|
| 1941 |
+
- ูุญูุธ ุชูุงุตูู ุงูู
ูุงู
|
| 1942 |
+
- Agent Loop ุงุญุชุฑุงูู
|
| 1943 |
- Tool Calling ู
ูุซูู
|
|
|
|
| 1944 |
- Streaming ุญูููู
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1945 |
""")
|
| 1946 |
|
| 1947 |
with gr.Row():
|
| 1948 |
+
user_id_box = gr.Textbox(
|
| 1949 |
+
label="๐ User ID (ุงุฎุชูุงุฑู โ ููุชู
ููุฒ ุจูู ุงูู
ุณุชุฎุฏู
ูู)",
|
| 1950 |
+
placeholder="ู
ุซุงู: user_123 ุฃู ุงุชุฑูู ูุงุฑุบุงู",
|
| 1951 |
+
scale=2,
|
| 1952 |
+
)
|
| 1953 |
model_dd = gr.Dropdown(
|
| 1954 |
choices=list(MODELS.keys()),
|
| 1955 |
value=CONFIG["default_model"],
|
| 1956 |
+
label="๐ค ุงููู
ูุฐุฌ",
|
| 1957 |
scale=2,
|
| 1958 |
)
|
| 1959 |
|
| 1960 |
+
with gr.Row():
|
| 1961 |
+
msg_box = gr.Textbox(
|
| 1962 |
+
label="ุฑุณุงูุชู",
|
| 1963 |
+
placeholder="ุงูุชุจ ููุง... ูู
ููู ุชุบููุฑ ุงูู
ูุถูุน ูู ุฃู ููุชุ ุงููููู ูุชุฐูุฑ ูู ุดูุก โจ",
|
| 1964 |
+
lines=3,
|
| 1965 |
+
scale=5,
|
| 1966 |
+
)
|
| 1967 |
+
send_btn = gr.Button("ุฅุฑุณุงู โถ", variant="primary", scale=1)
|
| 1968 |
+
|
| 1969 |
+
with gr.Row():
|
| 1970 |
+
clear_btn = gr.Button("๐๏ธ ู
ุณุญ ุงูุนุฑุถ (ูุง ูู
ุณุญ ุงูุฐุงูุฑุฉ)")
|
| 1971 |
+
|
| 1972 |
+
# ุฑุจุท ุงูุฃุญุฏุงุซ
|
| 1973 |
+
msg_box.submit(
|
| 1974 |
+
chat_handler,
|
| 1975 |
+
[msg_box, chatbot, model_dd, user_id_box],
|
| 1976 |
+
[chatbot],
|
| 1977 |
+
).then(lambda: "", None, [msg_box])
|
| 1978 |
+
|
| 1979 |
+
send_btn.click(
|
| 1980 |
+
chat_handler,
|
| 1981 |
+
[msg_box, chatbot, model_dd, user_id_box],
|
| 1982 |
+
[chatbot],
|
| 1983 |
+
).then(lambda: "", None, [msg_box])
|
| 1984 |
+
|
| 1985 |
clear_btn.click(lambda: [], None, [chatbot])
|
| 1986 |
|
| 1987 |
return demo
|
|
|
|
| 1989 |
demo = build_gradio_interface()
|
| 1990 |
app = gr.mount_gradio_app(app, demo, path="/chat")
|
| 1991 |
|
| 1992 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1993 |
# โโโ Entry Point โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1994 |
+
# โโโโโโโโ๏ฟฝ๏ฟฝโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 1995 |
|
| 1996 |
if __name__ == "__main__":
|
| 1997 |
import uvicorn
|
| 1998 |
|
| 1999 |
+
parser = argparse.ArgumentParser(description="Gemini Web2API + Zep Memory Agent")
|
| 2000 |
parser.add_argument("--port", type=int, default=CONFIG["port"])
|
| 2001 |
parser.add_argument("--host", type=str, default=CONFIG["host"])
|
| 2002 |
parser.add_argument("--cookie-file", type=str, default=None)
|
| 2003 |
parser.add_argument("--proxy", type=str, default=None)
|
| 2004 |
parser.add_argument("--max-iterations", type=int, default=CONFIG["agent_max_iterations"])
|
| 2005 |
+
parser.add_argument("--zep-key", type=str, default=None, help="Zep Cloud API Key")
|
| 2006 |
parser.add_argument("--version", action="version", version=f"v{__version__}")
|
| 2007 |
args = parser.parse_args()
|
| 2008 |
|
|
|
|
| 2010 |
CONFIG["cookie_file"] = args.cookie_file
|
| 2011 |
if args.proxy:
|
| 2012 |
CONFIG["proxy"] = args.proxy
|
| 2013 |
+
if args.zep_key:
|
| 2014 |
+
CONFIG["zep_api_key"] = args.zep_key
|
| 2015 |
CONFIG["agent_max_iterations"] = args.max_iterations
|
| 2016 |
|
| 2017 |
+
print(f"\n{'โ'*65}")
|
| 2018 |
+
print(f" gemini-web2api v{__version__} โ Professional Agent + Zep Memory")
|
| 2019 |
+
print(f"{'โ'*65}")
|
| 2020 |
print(f" API: http://localhost:{args.port}/v1")
|
| 2021 |
print(f" Chat UI: http://localhost:{args.port}/chat")
|
| 2022 |
+
print(f" Health: http://localhost:{args.port}/health")
|
| 2023 |
+
print(f" Memory: http://localhost:{args.port}/memory/status")
|
| 2024 |
print(f" Models: {', '.join(MODELS.keys())}")
|
| 2025 |
print(f" Agent Loop: max {CONFIG['agent_max_iterations']} iterations")
|
| 2026 |
+
print(f" Zep Memory: {'โ
Key configured' if CONFIG.get('zep_api_key') else 'โ ZEP_API_KEY not set'}")
|
| 2027 |
print(f" Cookie: {'โ
' if CONFIG.get('cookie_file') or CONFIG.get('_cookie_inline') else 'โ none (anonymous)'}")
|
| 2028 |
print(f" Proxy: {CONFIG.get('proxy') or 'โ none'}")
|
| 2029 |
+
print(f"{'โ'*65}\n")
|
| 2030 |
|
| 2031 |
uvicorn.run(
|
| 2032 |
app,
|