Skip to content

Commit fa8f316

Browse files
committed
docs(spec): add Telegram adapter notes
Signed-off-by: theonlychant <sacehenry@gmail.com>
1 parent caaf64a commit fa8f316

9 files changed

Lines changed: 551 additions & 0 deletions

File tree

docs/guides/telegram.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: Telegram Messaging Adapter
3+
---
4+
5+
This page documents the v0.18.2 Telegram messaging adapter scaffold.
6+
7+
Status: scaffolded — implementation work in progress.
8+
9+
Acceptance criteria (v0.18.2):
10+
11+
- `gaia telegram --token <TOKEN>` starts the adapter (scaffold currently logs and exits).
12+
- `/start` command replies within 5 seconds.
13+
- Streaming responses use message edits via `send_stream` from `AgentSDK`.
14+
15+
Next steps:
16+
17+
- Implement runtime using `python-telegram-bot` Application and handlers.
18+
- Wire uploads to VLM/RAG ingestion.
19+
- Add tests for allowed-users gate and restricted tools enforcement.

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@
160160
"starlette",
161161
"uvicorn",
162162
],
163+
"telegram": [
164+
"python-telegram-bot>=20.3",
165+
],
163166
"dev": [
164167
"pytest",
165168
"pytest-benchmark",

src/gaia/cli.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,50 @@ def main():
14081408
)
14091409
api_parser.set_defaults(action="api")
14101410

1411+
# Telegram adapter command (v0.18.2) - supports start|stop|status
1412+
telegram_parser = subparsers.add_parser(
1413+
"telegram",
1414+
help="Manage Telegram messaging adapter (start|stop|status)",
1415+
parents=[parent_parser],
1416+
)
1417+
telegram_subparsers = telegram_parser.add_subparsers(
1418+
dest="telegram_action", help="telegram action to perform"
1419+
)
1420+
1421+
# Start subcommand
1422+
t_start = telegram_subparsers.add_parser(
1423+
"start", help="Start the Telegram adapter (polling)"
1424+
)
1425+
t_start.add_argument("--token", required=True, help="Telegram bot token")
1426+
t_start.add_argument(
1427+
"--allowed-users",
1428+
help="Comma-separated Telegram user IDs allowed to interact (default: allow all)",
1429+
)
1430+
t_start.add_argument(
1431+
"--background",
1432+
action="store_true",
1433+
help="Run adapter in background/daemon mode (writes PID and health endpoint)",
1434+
)
1435+
1436+
# Stop subcommand
1437+
t_stop = telegram_subparsers.add_parser("stop", help="Stop background Telegram adapter")
1438+
t_stop.add_argument(
1439+
"--force",
1440+
action="store_true",
1441+
help="Force stop even if graceful shutdown fails",
1442+
)
1443+
1444+
# Status subcommand
1445+
t_status = telegram_subparsers.add_parser("status", help="Show status of Telegram adapter")
1446+
t_status.add_argument(
1447+
"--health-host", default="127.0.0.1", help="Health server host (default: 127.0.0.1)"
1448+
)
1449+
t_status.add_argument(
1450+
"--health-port", type=int, default=8765, help="Health server port (default: 8765)"
1451+
)
1452+
1453+
telegram_parser.set_defaults(action="telegram")
1454+
14111455
# Add model download command
14121456
download_parser = subparsers.add_parser(
14131457
"download",
@@ -2156,6 +2200,85 @@ def main():
21562200
)
21572201
return
21582202

2203+
# Handle telegram scaffold command
2204+
if args.action == "telegram":
2205+
# Telegram management: start | stop | status
2206+
action = getattr(args, "telegram_action", None)
2207+
if action == "start":
2208+
try:
2209+
from gaia.messaging.telegram import run_telegram
2210+
except Exception as e: # pragma: no cover - runtime import error
2211+
print(f"❌ Telegram support is not available: {e}", file=sys.stderr)
2212+
sys.exit(1)
2213+
2214+
allowed = None
2215+
if getattr(args, "allowed_users", None):
2216+
try:
2217+
allowed = set(int(x.strip()) for x in args.allowed_users.split(",") if x.strip())
2218+
except Exception:
2219+
print("Invalid --allowed-users format; expected comma-separated integers", file=sys.stderr)
2220+
sys.exit(2)
2221+
2222+
run_telegram(token=args.token, allowed_users=allowed, background=getattr(args, "background", False))
2223+
return
2224+
2225+
if action == "stop":
2226+
import signal
2227+
2228+
pid_path = os.path.expanduser("~/.gaia/telegram.pid")
2229+
if not os.path.exists(pid_path):
2230+
print("Telegram adapter is not running (no PID file).")
2231+
return
2232+
try:
2233+
with open(pid_path, "r", encoding="utf-8") as f:
2234+
pid = int(f.read().strip())
2235+
os.kill(pid, signal.SIGTERM)
2236+
print(f"Sent SIGTERM to Telegram adapter (pid {pid}).")
2237+
try:
2238+
os.remove(pid_path)
2239+
except OSError:
2240+
pass
2241+
except ProcessLookupError:
2242+
print("Process not found; removing stale PID file.")
2243+
try:
2244+
os.remove(pid_path)
2245+
except OSError:
2246+
pass
2247+
except PermissionError:
2248+
print("Permission denied when attempting to stop process. Try sudo.")
2249+
sys.exit(1)
2250+
except Exception as e:
2251+
print(f"Failed to stop Telegram adapter: {e}")
2252+
sys.exit(1)
2253+
return
2254+
2255+
if action == "status":
2256+
# Prefer health endpoint; fallback to pid file existence
2257+
import urllib.request
2258+
import urllib.error
2259+
2260+
host = getattr(args, "health_host", "127.0.0.1")
2261+
port = getattr(args, "health_port", 8765)
2262+
url = f"http://{host}:{port}/healthz"
2263+
try:
2264+
with urllib.request.urlopen(url, timeout=1) as resp:
2265+
body = resp.read().decode("utf-8").strip()
2266+
if resp.status == 200 and body == "ok":
2267+
print(f"Telegram adapter: healthy ({url})")
2268+
return
2269+
except urllib.error.URLError:
2270+
pass
2271+
2272+
pid_path = os.path.expanduser("~/.gaia/telegram.pid")
2273+
if os.path.exists(pid_path):
2274+
print("Telegram adapter: PID file exists, but health check failed (may be starting or unhealthy).")
2275+
else:
2276+
print("Telegram adapter: not running")
2277+
return
2278+
2279+
print("No telegram action specified. Use: gaia telegram start|stop|status", file=sys.stderr)
2280+
return
2281+
21592282
# Handle core Gaia CLI commands
21602283
if args.action in ["prompt", "chat", "talk", "stats"]:
21612284
kwargs = {

src/gaia/messaging/ingest.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Helpers to ingest uploaded media into VLM and RAG pipelines.
2+
3+
Provides small, testable wrappers around `VLMClient` and `RAGSDK` so
4+
messaging adapters can hand off downloaded files for processing.
5+
"""
6+
from __future__ import annotations
7+
8+
import logging
9+
import os
10+
from typing import Dict, Any
11+
12+
from gaia.llm.vlm_client import VLMClient
13+
from gaia.rag.sdk import RAGSDK, RAGConfig
14+
15+
log = logging.getLogger(__name__)
16+
17+
18+
def ingest_image_to_vlm(image_path: str, vlm_model: str = None) -> Dict[str, Any]:
19+
"""Run VLM extraction on an image file and return results.
20+
21+
Returns dict with keys: status, text, model, error (if any)
22+
"""
23+
if not os.path.exists(image_path):
24+
return {"status": "error", "error": "file_not_found", "path": image_path}
25+
26+
try:
27+
client = VLMClient(vlm_model=vlm_model) if vlm_model else VLMClient()
28+
image_bytes = open(image_path, "rb").read()
29+
text = client.extract_from_image(image_bytes)
30+
return {"status": "success", "text": text, "model": client.vlm_model}
31+
except Exception as e: # pragma: no cover - integration depends on Lemonade
32+
log.exception("VLM ingestion failed")
33+
return {"status": "error", "error": str(e), "path": image_path}
34+
35+
36+
def ingest_document_to_rag(file_path: str, config: RAGConfig | None = None) -> Dict[str, Any]:
37+
"""Index a document into the RAG index via RAGSDK.index_document.
38+
39+
Returns whatever `RAGSDK.index_document` returns (a dict of stats).
40+
"""
41+
cfg = config or RAGConfig()
42+
rag = RAGSDK(cfg)
43+
result = rag.index_document(file_path)
44+
return result

0 commit comments

Comments
 (0)