bahi-bh commited on
Commit
8cbefa0
ยท
verified ยท
1 Parent(s): b694792

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +890 -331
app.py CHANGED
@@ -1,19 +1,25 @@
1
  #!/usr/bin/env python3
2
  """
3
- gemini-web2api Enhanced โ€” Agent Edition
4
- ========================================
5
- ุงู„ู†ุณุฎุฉ ุงู„ู…ุญุณู‘ู†ุฉ ู…ุน ุฏุนู… ูƒุงู…ู„ ู„ู€ Agent Loop (ReAct) ูˆTool Calling ู…ูˆุซูˆู‚.
6
-
7
- ุงู„ุชุญุณูŠู†ุงุช ุงู„ุฌุฏูŠุฏุฉ:
8
- - โœ… ุฅุตู„ุงุญ MODELS dict (ุฎุทุฃ syntax ูƒุงู† ู…ูˆุฌูˆุฏุงู‹)
9
  - โœ… Agent Loop ุญู‚ูŠู‚ูŠ (ReAct: Reason โ†’ Act โ†’ Observe โ†’ Repeat)
10
- - โœ… System Prompt ู‚ูˆูŠ ูˆู…ููุตูŽู‘ู„ ู„ู„ุฃุฏูˆุงุช
11
- - โœ… JSON Tool Call parser ู…ุญุณู‘ู† ู…ุน fallback ู…ุชุนุฏุฏ ุงู„ู…ุณุชูˆูŠุงุช
12
- - โœ… Streaming ู…ุน Tool Calls (chunk-by-chunk)
13
- - โœ… ุญุฏ ุฃู‚ุตู‰ ู„ู„ุชูƒุฑุงุฑ (max_iterations) ู„ู…ู†ุน ุงู„ุญู„ู‚ุงุช ุงู„ู„ุงู†ู‡ุงุฆูŠุฉ
14
- - โœ… Tool result injection ุตุญูŠุญ ููŠ ู…ุญุงุฏุซุงุช Gemini
15
- - โœ… Parallel tool calls ุฏุนู…
16
- - โœ… ุชุณุฌูŠู„ ุชูุตูŠู„ูŠ ู„ูƒู„ ุฎุทูˆุฉ Agent
 
 
 
 
 
 
 
 
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
- __version__ = "2.1.0-agent"
 
 
 
 
 
 
 
51
 
 
52
  # โ”€โ”€โ”€ Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
53
 
54
  def load_config() -> dict:
55
  defaults = {
 
56
  "port": 7860,
57
  "host": "0.0.0.0",
58
- "retry_attempts": 3,
59
- "retry_delay_sec": 2,
60
- "request_timeout_sec": 180,
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
- # โœ… ุฌุฏูŠุฏ: ุฅุนุฏุงุฏุงุช Agent Loop
72
- "agent_max_iterations": 8, # ุญุฏ ุฃู‚ุตู‰ ู„ุฏูˆุฑุงุช ุงู„ุฃุฏุงุฉ
73
- "agent_max_tokens_per_step": 4096, # ุญุฏ ุงู„ุชูˆูƒู†ุฒ ู„ูƒู„ ุฎุทูˆุฉ
 
 
 
 
 
 
 
 
 
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 loading {p}: {e}")
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
- # โ”€โ”€โ”€ Models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
100
- # โœ… ุฅุตู„ุงุญ: ูƒุงู†ุช ู‡ู†ุงูƒ ุฃุฎุทุงุก syntax (ู‚ูˆุณ ู…ูู‚ูˆุฏ + ุชูƒุฑุงุฑ ู…ูุงุชูŠุญ)
 
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, longest output (~20k chars)",
118
  },
119
  "gemini-3.1-pro": {
120
  "mode": 3, "think": 4,
121
- "desc": "Pro model (requires cookie for real routing)",
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 with adaptive depth (~15k chars)",
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
- # โ”€โ”€โ”€ Production Services โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  @dataclass
160
  class RateLimitEntry:
@@ -240,15 +292,347 @@ class Services:
240
 
241
  services = Services()
242
 
243
- # โ”€โ”€โ”€ Logging โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
244
 
245
- def log(msg: str, level: str = "INFO"):
246
- if CONFIG.get("log_requests"):
247
- prefix = {"INFO": "โ„น๏ธ", "AGENT": "๐Ÿค–", "TOOL": "๐Ÿ”ง", "ERROR": "โŒ", "WARN": "โš ๏ธ"}.get(level, "โ€ข")
248
- sys.stderr.write(f"[{time.strftime('%H:%M:%S')}] {prefix} {msg}\n")
249
- sys.stderr.flush()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 and part[1] and isinstance(part[1], list):
 
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
- # โ”€โ”€โ”€ โœ… Tool Call Parser ุงู„ู…ุญุณู‘ู† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
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
- # โ”€โ”€โ”€ ู…ุณุชูˆู‰ 1: ```tool_call``` blocks
456
- pattern1 = r'```tool_call\s*\n(.*?)\n?```'
457
- for match in re.finditer(pattern1, text, re.DOTALL):
458
- raw = match.group(1).strip()
459
- tc = _try_parse_single_tool_call(raw)
460
  if tc:
461
  tool_calls.append(tc)
462
 
463
- # โ”€โ”€โ”€ ู…ุณุชูˆู‰ 2: ```json``` blocks ุงู„ุชูŠ ุชุญุชูˆูŠ ุนู„ู‰ tool call
464
  if not tool_calls:
465
- pattern2 = r'```json\s*\n(.*?)\n?```'
466
- for match in re.finditer(pattern2, text, re.DOTALL):
467
- raw = match.group(1).strip()
468
- tc = _try_parse_single_tool_call(raw)
469
  if tc:
470
  tool_calls.append(tc)
471
 
472
- # โ”€โ”€โ”€ ู…ุณุชูˆู‰ 3: XML-style <tool_call>
473
  if not tool_calls:
474
- pattern3 = r'<tool_call>(.*?)</tool_call>'
475
- for match in re.finditer(pattern3, text, re.DOTALL):
476
- raw = match.group(1).strip()
477
- tc = _try_parse_single_tool_call(raw)
478
  if tc:
479
  tool_calls.append(tc)
480
 
481
- # โ”€โ”€โ”€ ู…ุณุชูˆู‰ 4: JSON ู…ุจุงุดุฑ (ู„ู„ู†ู…ุงุฐุฌ ุงู„ุชูŠ ุชูุฎุฑุฌ JSON ู…ุจุงุดุฑุฉ)
482
  if not tool_calls:
483
- # ุงุจุญุซ ุนู† JSON objects ุชุญุชูˆูŠ ุนู„ู‰ "name" ูˆ "arguments"
484
- json_pattern = r'\{[^{}]*"name"\s*:\s*"[^"]+[^{}]*"arguments"\s*:[^{}]*\}'
485
- for match in re.finditer(json_pattern, text, re.DOTALL):
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
- # ุชู†ุธูŠู ุงู„ู†ุต ู…ู† ุฃุฌุฒุงุก tool calls
494
- clean = re.sub(pattern1, '', text, flags=re.DOTALL)
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
- def _try_parse_single_tool_call(raw: str) -> Optional[dict]:
508
- """ู…ุญุงูˆู„ุฉ ุชุญู„ูŠู„ tool call ูˆุงุญุฏ ู…ู† ู†ุต."""
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(raw_fixed)
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
- # โ”€โ”€โ”€ โœ… Tool/Agent System Prompt ุงู„ู…ุญุณู‘ู† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
553
 
554
- AGENT_SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools. You MUST use tools when they are needed to answer the user's request accurately.
555
 
556
- ## HOW TO USE TOOLS
 
 
 
 
557
 
558
- When you need to call a tool, you MUST respond with a tool_call block in this EXACT format:
 
559
 
560
  ```tool_call
561
  {"name": "tool_name_here", "arguments": {"param1": "value1", "param2": "value2"}}
562
  ```
563
 
564
- ## IMPORTANT RULES
565
-
566
- 1. **Always use tools when available** - Do not guess or make up information that a tool can provide.
567
- 2. **One tool at a time** - Call tools one by one unless you need multiple simultaneously.
568
- 3. **Wait for results** - After calling a tool, wait for the tool result before continuing.
569
- 4. **Use results** - Incorporate tool results into your final answer.
570
- 5. **Be explicit** - Always explain what tool you're calling and why.
571
 
572
  ## AVAILABLE TOOLS
573
 
574
  {tool_definitions}
575
 
576
- ## RESPONSE FORMAT
577
-
578
- - If you need a tool: respond with ONLY the tool_call block (no extra text before it).
579
- - If you have all information needed: provide your final answer directly.
580
- - Never call a tool you don't have access to.
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.get("properties", {}).items():
615
- required = pname in params.get("required", [])
616
  ptype = pinfo.get("type", "any")
617
  pdesc = pinfo.get("description", "")
618
- req_str = " *(required)*" if required else " *(optional)*"
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(messages: list, tools: list = None) -> str:
 
 
 
 
626
  """
627
- โœ… ู…ุญุณู‘ู†: ูŠุจู†ูŠ prompt ูˆุงุถุญุงู‹ ู…ุน system prompt ู‚ูˆูŠ ู„ู„ุฃุฏูˆุงุช.
 
 
 
628
  """
629
  parts = []
630
 
631
- # โ”€โ”€โ”€ System prompt ู„ู„ุฃุฏูˆุงุช
 
 
 
 
632
  if tools:
633
- tool_defs_text = build_tool_definitions_text(tools)
634
- system_tool_prompt = AGENT_SYSTEM_PROMPT.replace("{tool_definitions}", tool_defs_text)
635
- parts.append(f"[SYSTEM]\n{system_tool_prompt}")
 
 
 
 
 
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
- tool_calls_data = (msg.get("tool_calls") if isinstance(msg, dict)
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
- if not tools: # ุฅุฐุง ูƒุงู† ู‡ู†ุงูƒ ุฃุฏูˆุงุชุŒ system prompt ุงู„ุฃุฏูˆุงุช ูŠุฃุชูŠ ุฃูˆู„ุงู‹
653
- parts.append(f"[SYSTEM]\n{content}")
654
- else:
655
- # ุฏู…ุฌ system prompt ุงู„ู…ุณุชุฎุฏู… ู…ุน system prompt ุงู„ุฃุฏูˆุงุช
656
- parts[0] = parts[0] + f"\n\n## ADDITIONAL INSTRUCTIONS\n{content}"
 
 
657
 
658
  elif role == "assistant":
659
- if tool_calls_data:
660
- # ุชุถู…ูŠู† tool calls ุงู„ุณุงุจู‚ุฉ ููŠ ุงู„ุณูŠุงู‚
661
- tc_parts = [f"[ASSISTANT called tool(s)]:"]
662
- for tc in tool_calls_data:
663
- fn = tc.get("function", {})
664
- tc_parts.append(
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
- tc_parts.insert(1, f"[ASSISTANT]: {content}")
671
- parts.append("\n".join(tc_parts))
672
- else:
 
673
  parts.append(f"[ASSISTANT]: {content}")
674
 
675
  elif role == "tool":
676
- # โœ… ู†ุชุงุฆุฌ ุงู„ุฃุฏูˆุงุช ุจุดูƒู„ ูˆุงุถุญ
677
- tool_name = (msg.get("name", "") if isinstance(msg, dict)
678
- else getattr(msg, "name", ""))
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(f"[USER]: {content}")
 
 
 
689
 
690
- return "\n\n".join(p for p in parts if p)
691
 
692
- # โ”€โ”€โ”€ โœ… Agent Loop ุงู„ุฑุฆูŠุณูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
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 ุญู‚ูŠู‚ูŠ (ReAct pattern):
703
- 1. ุงุจู†ู prompt ู…ู† ุงู„ุณูŠุงู‚ ุงู„ูƒุงู…ู„
704
- 2. ุงุณุฃู„ Gemini
705
- 3. ุฅุฐุง ุฃุนุงุฏ tool call โ†’ ุฃุถู ุฅู„ู‰ ุงู„ุณูŠุงู‚ ูˆุงุนุฏ
706
- 4. ุฅุฐุง ู„ู… ูŠูุนุฏ tool call โ†’ ุฃุนุฏ ุงู„ุฑุฏ ุงู„ู†ู‡ุงุฆูŠ
707
 
708
- ูŠุนูŠุฏ: (final_text, final_tool_calls, updated_messages)
 
 
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
- while iteration < max_iterations:
718
- iteration += 1
719
  log(f"Agent iteration {iteration}/{max_iterations}", "AGENT")
720
 
721
- prompt = messages_to_prompt(current_messages, tools if iteration == 1 else tools)
722
- raw = gemini_stream_generate(prompt, model_id, think_mode)
723
- text = extract_response_text(raw)
 
 
 
 
 
 
724
 
725
  if not text:
726
  log("Empty response from Gemini", "WARN")
 
 
727
  break
728
 
729
- # โ”€โ”€โ”€ ู…ุญุงูˆู„ุฉ ุชุญู„ูŠู„ tool calls
730
  clean_text, tool_calls = parse_tool_calls(text)
731
 
732
  if tool_calls and tools:
733
- log(f"Tool calls detected: {[tc['function']['name'] for tc in tool_calls]}", "TOOL")
 
734
 
735
- # ุฃุถู ุฑุฏ ุงู„ู…ุณุงุนุฏ ู…ุน tool calls ุฅู„ู‰ ุงู„ุณูŠุงู‚
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
- # ููŠ ูˆุถุน non-streamingุŒ ู†ูุนูŠุฏ tool calls ู„ู„ู…ุณุชุฏุนูŠ
745
- # ููŠ ูˆุถุน agent ุฏุงุฎู„ูŠุŒ ู†ุถูŠู placeholder
746
- return clean_text, tool_calls, current_messages
747
 
748
  else:
749
- # โ”€โ”€โ”€ ุฑุฏ ู†ู‡ุงุฆูŠ ุจุฏูˆู† tool calls
750
- log(f"Final response after {iteration} iterations", "AGENT")
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 run_agent_with_tool_execution_sync(
758
  messages: List[dict],
759
  model_id: int,
760
  think_mode: int,
761
  tools: Optional[List[dict]],
762
- tool_executor=None, # ุฏุงู„ุฉ ู„ุชู†ููŠุฐ ุงู„ุฃุฏูˆุงุช (ุงุฎุชูŠุงุฑูŠุฉ)
 
763
  max_iterations: int = None,
764
  ) -> Tuple[str, Optional[List[dict]], List[dict]]:
765
  """
766
- โœ… Agent Loop ุงู„ูƒุงู…ู„ ู…ุน ุชู†ููŠุฐ ุงู„ุฃุฏูˆุงุช ุชู„ู‚ุงุฆูŠุงู‹ (ุฅุฐุง ุชูˆูู‘ุฑ executor).
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 loop iteration {iteration}/{max_iterations}", "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 call failed: {e}", "ERROR")
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 tool: {fn_name}({fn_args})", "TOOL")
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 execution error: {e}", "ERROR")
821
  else:
822
- # ู„ุง ูŠูˆุฌุฏ executorุŒ ุฃุถู placeholder
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, model_id: int, think_mode: int, tools: list = None
 
 
 
842
  ) -> Tuple[str, Optional[list]]:
843
- """ุงุณุชุฏุนุงุก ู…ุฒุงู…ู† ุจุณูŠุท (ุจุฏูˆู† agent loop)."""
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 for Google Gemini Web with full Agent/Tool support",
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}:{context_str}:{body.user}:{bool(tools)}".encode()
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, tool_calls, _ = await loop.run_in_executor(
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 tool_calls:
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": [{"index": 0, "delta": msg_delta, "finish_reason": "tool_calls"}],
 
 
 
 
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": [{"index": 0, "delta": {"role": "assistant", "content": full_text}, "finish_reason": None}],
 
 
 
 
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": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}],
 
 
 
 
1033
  }
1034
  yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
1035
 
1036
  # Final chunk
1037
- finish = "tool_calls" if tools and full_text else "stop"
1038
- final_chunk = {
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
- # Cache ุงู„ู†ุต ุงู„ู†ู‡ุงุฆูŠ (ุจุฏูˆู† tool calls)
1047
- if full_text and not tools:
1048
- resp_data = _build_completion_response(cid, created, model_name, full_text, None, "")
1049
- await services.set_cache(cache_key, json.dumps(resp_data))
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
- # โœ… ุงุณุชุฎุฏู… Agent Loop
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, prompt, model_id, think_mode, None
 
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, messages, model_id, think_mode, tools,
1205
- CONFIG["agent_max_iterations"]
 
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 _google_contents_to_prompt(req: dict) -> str:
1274
- parts = []
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
- parts.append(f"[SYSTEM]\n{sys_text}")
1280
  for content in req.get("contents", []):
1281
- role = content.get("role", "user")
1282
  text = " ".join(p.get("text", "") for p in content.get("parts", []) if p.get("text"))
1283
- if role == "model":
1284
- parts.append(f"[ASSISTANT]: {text}")
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
- stream = ":streamGenerateContent" in model_path
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
- prompt = _google_contents_to_prompt(req)
1300
- if not prompt.strip():
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 stream:
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": ["/v1/chat/completions", "/v1/models", "/v1/responses", "/v1beta/models"],
 
 
 
 
 
 
 
1349
  }
1350
 
1351
  @app.get("/health")
1352
  async def health():
1353
- return {"status": "healthy", "timestamp": time.time(), "version": __version__}
 
 
 
 
 
1354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1355
  # โ”€โ”€โ”€ Gradio UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
1356
 
1357
  def build_gradio_interface():
1358
- async def chat_handler(message: str, history: list, model_choice: str, request: gr.Request):
 
 
 
 
 
 
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
- history.append((message, ""))
 
 
 
 
 
 
1370
  yield history
1371
 
1372
  full_response = ""
1373
  try:
1374
- prompt = messages_to_prompt([{"role": "user", "content": message}])
 
 
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
- api_endpoint = "https://your-space.hf.space/v1"
 
 
 
 
 
 
 
 
 
1385
  api_key_display = CONFIG.get("api_keys", ["sk-gemini"])[0]
1386
 
1387
- with gr.Blocks(title="Gemini Web2API โ€” Agent Edition") as demo:
 
 
 
1388
  gr.Markdown(f"""
1389
- # ๐Ÿค– Gemini Web2API โ€” Agent Edition v{__version__}
1390
- > OpenAI-compatible proxy ู…ุน ุฏุนู… ูƒุงู…ู„ ู„ู€ Agent Loop ูˆTool Calling
1391
-
1392
- | | |
1393
- |---|---|
1394
- | **Base URL** | `{api_endpoint}` |
1395
- | **API Key** | `{api_key_display}` |
1396
- | **Models** | {', '.join(f'`{m}`' for m in MODELS.keys())} |
1397
- | **Agent Loop** | โœ… ุญุชู‰ {CONFIG['agent_max_iterations']} iterations |
1398
  """)
1399
 
1400
  with gr.Row():
1401
  with gr.Column(scale=4):
1402
- chatbot = gr.Chatbot(height=500, label="ุงู„ู…ุญุงุฏุซุฉ")
 
 
 
 
1403
  with gr.Column(scale=1):
1404
- gr.Markdown("""### โœ… ุงู„ู…ู…ูŠุฒุงุช
1405
- - Agent Loop (ReAct)
 
 
 
 
 
 
 
 
 
 
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
- msg_box = gr.Textbox(label="ุฑุณุงู„ุชูƒ", placeholder="ุงูƒุชุจ ู‡ู†ุง...", lines=2, scale=5)
1417
- send_btn = gr.Button("ุฅุฑุณุงู„ โ–ถ", variant="primary", scale=1)
1418
-
1419
- with gr.Row():
1420
- clear_btn = gr.Button("๐Ÿ—‘๏ธ ู…ุณุญ")
1421
  model_dd = gr.Dropdown(
1422
  choices=list(MODELS.keys()),
1423
  value=CONFIG["default_model"],
1424
- label="ุงู„ู†ู…ูˆุฐุฌ",
1425
  scale=2,
1426
  )
1427
 
1428
- msg_box.submit(chat_handler, [msg_box, chatbot, model_dd], [chatbot]).then(
1429
- lambda: "", None, [msg_box]
1430
- )
1431
- send_btn.click(chat_handler, [msg_box, chatbot, model_dd], [chatbot]).then(
1432
- lambda: "", None, [msg_box]
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 Agent Edition")
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{'='*60}")
1462
- print(f" gemini-web2api v{__version__} โ€” Agent Edition")
1463
- print(f"{'='*60}")
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"{'='*60}\n")
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,