Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/deployment/code-signing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,5 @@ For Windows, SignPath's OSS tier is GitHub-only — you can't sign locally. Use
- SignPath OSS Foundation: [https://signpath.io/solutions/open-source-community](https://signpath.io/solutions/open-source-community)
- SignPath GitHub Action: [https://github.com/signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request)
- Apple notarization docs: [https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
- electron-builder code signing: [https://www.electron.build/code-signing](https://www.electron.build/code-signing)
- electron-builder code signing: [https://github.com/electron-userland/electron-builder](https://github.com/electron-userland/electron-builder)
- GAIA desktop installer plan: [`docs/plans/desktop-installer.mdx`](../plans/desktop-installer)
19 changes: 19 additions & 0 deletions docs/guides/telegram.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Telegram Messaging Adapter
---

This page documents the v0.18.2 Telegram messaging adapter scaffold.

Status: scaffolded — implementation work in progress.

Acceptance criteria (v0.18.2):

- `gaia telegram --token <TOKEN>` starts the adapter (scaffold currently logs and exits).
- `/start` command replies within 5 seconds.
- Streaming responses use message edits via `send_stream` from `AgentSDK`.

Next steps:

- Implement runtime using `python-telegram-bot` Application and handlers.
- Wire uploads to VLM/RAG ingestion.
- Add tests for allowed-users gate and restricted tools enforcement.
4 changes: 2 additions & 2 deletions docs/plans/desktop-installer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -892,8 +892,8 @@ v0.17.2 release with desktop installer as the primary install path
- Related issue (left as polish, not folded in): [#597 — First-run setup wizard](https://github.com/amd/gaia/issues/597)
- Related stale PR (folded into Phase B): [#341 — Fix installer BOM and logger permissions](https://github.com/amd/gaia/pull/341)
- Lemonade installer source (reference architecture): [lemonade-sdk/lemonade](https://github.com/lemonade-sdk/lemonade)
- electron-builder docs: [https://www.electron.build/](https://www.electron.build/)
- electron-updater docs: [https://www.electron.build/auto-update](https://www.electron.build/auto-update)
- electron-builder (repo & docs): [https://github.com/electron-userland/electron-builder](https://github.com/electron-userland/electron-builder)
- electron-updater / auto-update: [https://github.com/electron-userland/electron-builder#auto-update](https://github.com/electron-userland/electron-builder#auto-update)
- NSIS docs: [https://nsis.sourceforge.io/Docs/](https://nsis.sourceforge.io/Docs/)
- SignPath OSS program: [https://signpath.io/solutions/open-source-community](https://signpath.io/solutions/open-source-community)
- Apple notarization: [https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
2 changes: 1 addition & 1 deletion installer/nsis/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
; customInstall — runs at the end of the main install section
; customUnInstall — runs at the start of the uninstall section
;
; Reference: https://www.electron.build/configuration/nsis.html#custom-nsis-script
; Reference: https://github.com/electron-userland/electron-builder

; ─── File-scope includes ───────────────────────────────────────────────
;
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@
"starlette",
"uvicorn",
],
"telegram": [
"python-telegram-bot>=20.3",
],
"dev": [
"pytest",
"pytest-benchmark",
Expand Down
148 changes: 148 additions & 0 deletions src/gaia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,59 @@ def main():
)
api_parser.set_defaults(action="api")

# Telegram adapter command (v0.18.2) - supports start|stop|status
telegram_parser = subparsers.add_parser(
"telegram",
help="Manage Telegram messaging adapter (start|stop|status)",
parents=[parent_parser],
)
telegram_subparsers = telegram_parser.add_subparsers(
dest="telegram_action", help="telegram action to perform"
)

# Start subcommand
t_start = telegram_subparsers.add_parser(
"start", help="Start the Telegram adapter (polling)"
)
t_start.add_argument("--token", required=True, help="Telegram bot token")
t_start.add_argument(
"--allowed-users",
help="Comma-separated Telegram user IDs allowed to interact (default: allow all)",
)
t_start.add_argument(
"--background",
action="store_true",
help="Run adapter in background/daemon mode (writes PID and health endpoint)",
)

# Stop subcommand
t_stop = telegram_subparsers.add_parser(
"stop", help="Stop background Telegram adapter"
)
t_stop.add_argument(
"--force",
action="store_true",
help="Force stop even if graceful shutdown fails",
)

# Status subcommand
t_status = telegram_subparsers.add_parser(
"status", help="Show status of Telegram adapter"
)
t_status.add_argument(
"--health-host",
default="127.0.0.1",
help="Health server host (default: 127.0.0.1)",
)
t_status.add_argument(
"--health-port",
type=int,
default=8765,
help="Health server port (default: 8765)",
)

telegram_parser.set_defaults(action="telegram")

# Add model download command
download_parser = subparsers.add_parser(
"download",
Expand Down Expand Up @@ -2156,6 +2209,101 @@ def main():
)
return

# Handle telegram scaffold command
if args.action == "telegram":
# Telegram management: start | stop | status
action = getattr(args, "telegram_action", None)
if action == "start":
try:
from gaia.messaging.telegram import run_telegram
except Exception as e: # pragma: no cover - runtime import error
print(f"❌ Telegram support is not available: {e}", file=sys.stderr)
sys.exit(1)

allowed = None
if getattr(args, "allowed_users", None):
try:
allowed = set(
int(x.strip())
for x in args.allowed_users.split(",")
if x.strip()
)
except Exception:
print(
"Invalid --allowed-users format; expected comma-separated integers",
file=sys.stderr,
)
sys.exit(2)

run_telegram(
token=args.token,
allowed_users=allowed,
background=getattr(args, "background", False),
)
return

if action == "stop":
import signal

pid_path = os.path.expanduser("~/.gaia/telegram.pid")
if not os.path.exists(pid_path):
print("Telegram adapter is not running (no PID file).")
return
try:
with open(pid_path, "r", encoding="utf-8") as f:
pid = int(f.read().strip())
os.kill(pid, signal.SIGTERM)
print(f"Sent SIGTERM to Telegram adapter (pid {pid}).")
try:
os.remove(pid_path)
except OSError:
pass
except ProcessLookupError:
print("Process not found; removing stale PID file.")
try:
os.remove(pid_path)
except OSError:
pass
except PermissionError:
print("Permission denied when attempting to stop process. Try sudo.")
sys.exit(1)
except Exception as e:
print(f"Failed to stop Telegram adapter: {e}")
sys.exit(1)
return

if action == "status":
# Prefer health endpoint; fallback to pid file existence
import urllib.error
import urllib.request

host = getattr(args, "health_host", "127.0.0.1")
port = getattr(args, "health_port", 8765)
url = f"http://{host}:{port}/healthz"
try:
with urllib.request.urlopen(url, timeout=1) as resp:
body = resp.read().decode("utf-8").strip()
if resp.status == 200 and body == "ok":
print(f"Telegram adapter: healthy ({url})")
return
except urllib.error.URLError:
pass

pid_path = os.path.expanduser("~/.gaia/telegram.pid")
if os.path.exists(pid_path):
print(
"Telegram adapter: PID file exists, but health check failed (may be starting or unhealthy)."
)
else:
print("Telegram adapter: not running")
return

print(
"No telegram action specified. Use: gaia telegram start|stop|status",
file=sys.stderr,
)
return

# Handle core Gaia CLI commands
if args.action in ["prompt", "chat", "talk", "stats"]:
kwargs = {
Expand Down
47 changes: 47 additions & 0 deletions src/gaia/messaging/ingest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Helpers to ingest uploaded media into VLM and RAG pipelines.

Provides small, testable wrappers around `VLMClient` and `RAGSDK` so
messaging adapters can hand off downloaded files for processing.
"""

from __future__ import annotations

import logging
import os
from typing import Any, Dict

from gaia.llm.vlm_client import VLMClient
from gaia.rag.sdk import RAGSDK, RAGConfig

log = logging.getLogger(__name__)


def ingest_image_to_vlm(image_path: str, vlm_model: str = None) -> Dict[str, Any]:
"""Run VLM extraction on an image file and return results.

Returns dict with keys: status, text, model, error (if any)
"""
if not os.path.exists(image_path):
return {"status": "error", "error": "file_not_found", "path": image_path}

try:
client = VLMClient(vlm_model=vlm_model) if vlm_model else VLMClient()
image_bytes = open(image_path, "rb").read()
text = client.extract_from_image(image_bytes)
return {"status": "success", "text": text, "model": client.vlm_model}
except Exception as e: # pragma: no cover - integration depends on Lemonade
log.exception("VLM ingestion failed")
return {"status": "error", "error": str(e), "path": image_path}


def ingest_document_to_rag(
file_path: str, config: RAGConfig | None = None
) -> Dict[str, Any]:
"""Index a document into the RAG index via RAGSDK.index_document.

Returns whatever `RAGSDK.index_document` returns (a dict of stats).
"""
cfg = config or RAGConfig()
rag = RAGSDK(cfg)
result = rag.index_document(file_path)
return result
Loading
Loading