Files
ai-agents/dashboard/models.py
T
Eric Jungbauer 043aa18f3f API Clients + structured JSON results: app-level tokens for Synap/WSIT integration
- New api_clients + api_client_scopes tables; tokens scoped per-instance
- Admin UI tab at /admin for token create/rotate/revoke/delete with one-time reveal
- Dual-auth dependency (user session OR Bearer app token) on trigger + runs endpoints
- /api/instances/{id}/trigger pre-creates a run and returns run_id + cached last_result instantly
- New GET /api/runs/{id} for polling
- Generic trigger path for sub-agent instances (weather, calendar, etc.)
- runs.result column for structured JSON alongside markdown output
- agent_catalog.result_schema describes each agent's result shape
- Weather, daily-briefing, project-monitor retrofitted to emit structured results
- log_run: env INSTANCE_ID/RUN_ID only used when target matches, so nested sub-agents don't clobber parent runs
- Wiki docs: API Clients & Token Scoping + Calling Agents From Your Apps
2026-04-20 17:54:32 +00:00

155 lines
6.7 KiB
Python

from sqlalchemy import Column, String, Text, DateTime, Integer, Boolean, ForeignKey, JSON
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=True)
password_hash = Column(String, nullable=False)
display_name = Column(String, default="")
role = Column(String, default="user") # admin or user
llm_config = Column(JSON, default=dict) # user's own LLM provider config
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
instances = relationship("AgentInstance", back_populates="user")
class AgentCatalog(Base):
__tablename__ = "agent_catalog"
id = Column(String, primary_key=True)
name = Column(String, nullable=False)
description = Column(Text, default="")
category = Column(String, default="utility") # data, briefing, utility
config_schema = Column(JSON, default=dict)
default_config = Column(JSON, default=dict)
supports_schedule = Column(Boolean, default=True)
is_sub_agent = Column(Boolean, default=False)
requires_llm = Column(Boolean, default=False)
result_schema = Column(JSON, default=dict) # shape of the agent's structured result
instances = relationship("AgentInstance", back_populates="catalog_entry")
class AgentInstance(Base):
__tablename__ = "agent_instances"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
catalog_id = Column(String, ForeignKey("agent_catalog.id"), nullable=False)
name = Column(String, nullable=False)
config = Column(JSON, default=dict)
schedule = Column(String, default="manual")
status = Column(String, default="active")
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
user = relationship("User", back_populates="instances")
catalog_entry = relationship("AgentCatalog", back_populates="instances")
runs = relationship("Run", back_populates="instance", order_by="Run.started_at.desc()")
class Run(Base):
__tablename__ = "runs"
id = Column(Integer, primary_key=True, autoincrement=True)
instance_id = Column(Integer, ForeignKey("agent_instances.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
started_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
finished_at = Column(DateTime, nullable=True)
status = Column(String, default="running")
output = Column(Text, default="") # markdown rendering (for wiki)
result = Column(JSON, nullable=True) # structured data for API consumers
error = Column(Text, default="")
metadata_ = Column("metadata", JSON, default=dict)
triggered_by = Column(String, default="") # "user:eric", "api_client:synap", "cron"
instance = relationship("AgentInstance", back_populates="runs")
class Bridge(Base):
__tablename__ = "bridges"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
api_key = Column(String, nullable=False) # Auth token for bridge requests
bridge_url = Column(String, default="") # http://ip:port
hostname = Column(String, default="") # e.g. "Jungbauers-MBP"
platform = Column(String, default="macos") # macos, ios (future)
capabilities = Column(JSON, default=list) # ["notes", "reading-list"]
status = Column(String, default="offline") # online, offline
last_heartbeat = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
user = relationship("User")
class RouteLog(Base):
__tablename__ = "route_log"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
request_text = Column(Text, nullable=False)
recommended_agent = Column(String, default="")
action = Column(String, default="")
reasoning = Column(Text, default="")
outcome = Column(String, default="pending") # pending, accepted, rejected, success, failed
metadata_ = Column("metadata", JSON, default=dict)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class LLMProvider(Base):
__tablename__ = "llm_providers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
provider_type = Column(String, default="anthropic") # anthropic, openai, litellm, ollama
api_url = Column(String, default="")
api_key = Column(String, default="")
default_model = Column(String, default="")
is_default = Column(Boolean, default=False)
class APIClient(Base):
"""App-level API client. Each external app (Synap, WSIT, etc.) gets one.
Scoped to specific agent instances — see APIClientScope."""
__tablename__ = "api_clients"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False, unique=True) # "Synap", "WSIT"
token_hash = Column(String, nullable=False, unique=True) # SHA-256 of the plaintext token
token_prefix = Column(String, default="") # first 8 chars of token, shown in admin UI
description = Column(Text, default="")
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
last_used_at = Column(DateTime, nullable=True)
revoked_at = Column(DateTime, nullable=True)
scopes = relationship("APIClientScope", back_populates="client", cascade="all, delete-orphan")
class APIClientScope(Base):
"""Join table: which instances can an API client trigger/read?"""
__tablename__ = "api_client_scopes"
id = Column(Integer, primary_key=True, autoincrement=True)
api_client_id = Column(Integer, ForeignKey("api_clients.id", ondelete="CASCADE"), nullable=False)
instance_id = Column(Integer, ForeignKey("agent_instances.id", ondelete="CASCADE"), nullable=False)
client = relationship("APIClient", back_populates="scopes")
class APIClientCall(Base):
"""Audit log for every authenticated API call by an API client."""
__tablename__ = "api_client_calls"
id = Column(Integer, primary_key=True, autoincrement=True)
api_client_id = Column(Integer, ForeignKey("api_clients.id", ondelete="CASCADE"), nullable=False)
instance_id = Column(Integer, nullable=True) # may be null for non-instance endpoints
endpoint = Column(String, default="") # e.g. "POST /api/instances/2/trigger"
status_code = Column(Integer, default=0)
called_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))