Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
195 changes: 195 additions & 0 deletions anton/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,180 @@ def _extract_html_title(path, re_module) -> str:
return ""


async def _handle_remote(
console: Console,
settings,
workspace,
) -> None:
"""Handle /remote command — provision or check status of remote scratchpad."""
# import asyncio
# import json
# import os
# import time
# from urllib.request import Request, urlopen

# from rich.live import Live
# from rich.spinner import Spinner

console.print()

from pathlib import Path as _P
from anton.workspace import Workspace as _W
_global_ws = _W(_P.home())

# Ensure minds API key — same flow as /publish
if not settings.minds_api_key:
import webbrowser
from anton.utils.prompt import prompt_or_cancel

console.print(" [anton.muted]To use remote scratchpad you need a free Minds account.[/]")
console.print()
has_key = await prompt_or_cancel(
" Do you have an mdb.ai API key?",
choices=["y", "n"],
choices_display="y/n",
default="y",
)
if has_key is None:
console.print()
return
if has_key.lower() == "n":
webbrowser.open(
"https://mdb.ai/auth/realms/mindsdb/protocol/openid-connect/registrations"
"?client_id=public-client&response_type=code&scope=openid"
"&redirect_uri=https%3A%2F%2Fmdb.ai"
)
console.print()

api_key_input = await prompt_or_cancel(" API key", password=True)
if api_key_input is None or not api_key_input.strip():
console.print()
return
api_key_input = api_key_input.strip()
settings.minds_api_key = api_key_input

_global_ws.set_secret("ANTON_MINDS_API_KEY", api_key_input)
console.print()

# If an API key is provided, set the backend to remote
if settings.minds_api_key:
_global_ws.set_secret("ANTON_BACKEND", "remote")

# TODO: This code was used for provisioning remote scratchpads via
# 4nton.ai, but is no longer used. Should it be removed?
# provision_url = settings.publish_url.rstrip("/") + "/provision"
# api_key = settings.minds_api_key

# # Check current status via GET /provision
# with Live(Spinner("dots", text=" Checking remote scratchpad...", style="anton.cyan"), console=console, transient=True):
# try:
# req = Request(
# provision_url,
# headers={
# "Authorization": f"Bearer {api_key}",
# "User-Agent": "anton/1.0",
# },
# )
# with urlopen(req, timeout=15) as resp:
# result = json.loads(resp.read().decode())
# except Exception as e:
# console.print(f" [anton.error]Failed to check status: {e}[/]")
# console.print()
# return

# status = result.get("status", "none")
# endpoint = result.get("endpoint", "")

# # Already running — save and confirm
# if status == "running":
# console.print(f" [anton.success]Remote scratchpad is running[/]")
# console.print(f" [anton.muted]{endpoint}[/]")
# console.print()
# from pathlib import Path as _P
# from anton.workspace import Workspace as _W
# _W(_P.home()).set_secret("ANTON_REMOTE_SCRATCHPAD_URL", endpoint)
# os.environ["ANTON_REMOTE_SCRATCHPAD_URL"] = endpoint
# return

# # Not provisioned — start provisioning
# if status in ("none", "stopped"):
# action = "Waking up" if status == "stopped" else "Provisioning"
# console.print(f" [anton.muted]{action} remote scratchpad...[/]")

# with Live(Spinner("dots", text=f" {action}...", style="anton.cyan"), console=console, transient=True):
# try:
# req = Request(
# provision_url,
# data=b"{}",
# method="POST",
# headers={
# "Authorization": f"Bearer {api_key}",
# "Content-Type": "application/json",
# "User-Agent": "anton/1.0",
# },
# )
# with urlopen(req, timeout=30) as resp:
# result = json.loads(resp.read().decode())
# except Exception as e:
# console.print(f" [anton.error]Provisioning failed: {e}[/]")
# console.print()
# return

# endpoint = result.get("endpoint", endpoint)
# status = result.get("status", "")

# # Poll until ready — 5s intervals, 3 min max
# # Use /resolve to get the direct IP, then poll /health on it
# if status in ("provisioning", "starting"):
# max_wait = 180
# poll_interval = 5
# start_time = time.time()
# direct_endpoint = None

# with Live(Spinner("dots", text=" Waiting for instance to be ready...", style="anton.cyan"), console=console, transient=True):
# while time.time() - start_time < max_wait:
# await asyncio.sleep(poll_interval)
# try:
# # First resolve the direct IP via Cloudflare Worker
# if not direct_endpoint:
# req = Request(
# f"{endpoint}/resolve",
# headers={"Authorization": f"Bearer {api_key}", "User-Agent": "anton/1.0"},
# )
# with urlopen(req, timeout=5) as resp:
# resolve_data = json.loads(resp.read().decode())
# if resolve_data.get("status") == "running":
# direct_endpoint = resolve_data.get("endpoint", "")

# # Then check health directly
# if direct_endpoint:
# req = Request(
# f"{direct_endpoint}/health",
# headers={"Authorization": f"Bearer {api_key}", "User-Agent": "anton/1.0"},
# )
# with urlopen(req, timeout=5) as resp:
# health = json.loads(resp.read().decode())
# if health.get("status") == "ok":
# break
# except Exception:
# pass
# else:
# console.print(" [anton.warning]Instance is still setting up (3+ minutes).[/]")
# console.print(" [anton.muted]Run /remote again in a few minutes.[/]")
# console.print()
# return

# Save and confirm
console.print(f" [anton.success]Remote scratchpad ready![/]")
# console.print(f" [link={endpoint}]{endpoint}[/link]")
console.print()

from pathlib import Path as _P
from anton.workspace import Workspace as _W
# _W(_P.home()).set_secret("ANTON_REMOTE_SCRATCHPAD_URL", endpoint)
# os.environ["ANTON_REMOTE_SCRATCHPAD_URL"] = endpoint


async def _handle_publish(
console: Console,
settings,
Expand Down Expand Up @@ -374,6 +548,9 @@ async def _handle_publish(
settings.minds_api_key = api_key
# Key is not persisted yet — wait until publish succeeds to avoid
# locking the user out with a bad key on every subsequent /publish call.
from pathlib import Path as _P
from anton.workspace import Workspace as _W
_W(_P.home()).set_secret("ANTON_MINDS_API_KEY", api_key)
console.print()

# 2. Find the HTML file to publish
Expand Down Expand Up @@ -1051,8 +1228,11 @@ async def _chat_loop(
runtime_context = build_runtime_context(settings)

output_path = f"{settings.output_dir.rstrip('/')}/"
from anton.chat_session import get_runtime_factory

session = ChatSession(ChatSessionConfig(
llm_client=state["llm_client"],
runtime_factory=get_runtime_factory(settings),
self_awareness=self_awareness,
cortex=cortex,
episodic=episodic,
Expand Down Expand Up @@ -1366,6 +1546,21 @@ def _bottom_toolbar():
arg = parts[1].strip() if len(parts) > 1 else ""
handle_theme(console, arg)
continue
elif cmd == "/remote":
await _handle_remote(console, settings, workspace)
# Rebuild session so scratchpad uses remote/local factory
session = rebuild_session(
settings=settings,
state=state,
self_awareness=self_awareness,
cortex=cortex,
workspace=workspace,
console=console,
episodic=episodic,
history_store=history_store,
session_id=current_session_id,
)
continue
elif cmd == "/publish":
arg = parts[1].strip() if len(parts) > 1 else ""
await _handle_publish(console, settings, workspace, arg)
Expand Down
22 changes: 22 additions & 0 deletions anton/chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ def build_runtime_context(settings: AntonSettings) -> str:
return ctx


def get_runtime_factory(settings: AntonSettings):
"""Return the appropriate scratchpad runtime factory based on settings.

If backend is set to "remote" (and minds_api_key available),
returns a remote factory. Otherwise returns the local factory.
"""
print(settings.backend)
if settings.backend == "remote":
from functools import partial
from anton.core.backends.remote import remote_scratchpad_runtime_factory

return partial(
remote_scratchpad_runtime_factory,
endpoint_url=settings.minds_url,
api_key=settings.minds_api_key,
)

from anton.core.backends.local import local_scratchpad_runtime_factory
return local_scratchpad_runtime_factory


def rebuild_session(
*,
settings: AntonSettings,
Expand Down Expand Up @@ -84,6 +105,7 @@ def rebuild_session(
output_path = f"{settings.output_dir.rstrip('/')}/"
return ChatSession(ChatSessionConfig(
llm_client=state["llm_client"],
runtime_factory=get_runtime_factory(settings),
self_awareness=self_awareness,
cortex=cortex,
episodic=episodic,
Expand Down
5 changes: 4 additions & 1 deletion anton/chat_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,12 @@ def _print_done_line(
line.append(" \u2714 ", style="green")
work_str = self._fmt_elapsed(work_elapsed)

import os
work_label = "Remote work" if os.environ.get("ANTON_REMOTE_SCRATCHPAD_URL") else "Worked"

if reasoning_elapsed > 0:
reason_str = self._fmt_elapsed(reasoning_elapsed)
line.append(f"(Worked: {work_str}, Reasoned: {reason_str})", style="anton.muted")
line.append(f"({work_label}: {work_str}, Reasoned: {reason_str})", style="anton.muted")
else:
line.append(work_str, style="anton.muted")

Expand Down
1 change: 1 addition & 0 deletions anton/commands/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Command:
"Chat Tools",
Command("/paste", "Attach an image from your clipboard"),
Command("/resume", "Continue a previous session"),
Command("/remote", "Set up or manage remote scratchpad"),
Command("/publish", "Publish an HTML report to the web"),
Command("/unpublish", "Remove a published report"),
Command("/explain", "Show explainability details for the latest answer"),
Expand Down
11 changes: 10 additions & 1 deletion anton/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pathlib import Path

from pydantic import PrivateAttr, field_validator
from pydantic import PrivateAttr, ValidationInfo, field_validator
from pydantic_settings import BaseSettings

from anton.core.settings import CoreSettings
Expand Down Expand Up @@ -74,6 +74,15 @@ class AntonSettings(CoreSettings):
# Publish service
publish_url: str = "https://4nton.ai"

backend: str = "local" # local | remote

@field_validator("backend", mode="after")
@classmethod
def _validate_backend(cls, v: str, info: ValidationInfo) -> str:
if v == "remote" and not info.data.get("minds_api_key"):
raise ValueError("Minds API key is required for remote backend")
return v

@field_validator("minds_ssl_verify", mode="before")
@classmethod
def _parse_minds_ssl_verify(cls, v):
Expand Down
Loading
Loading