models.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """
  2. agentpaas.db.models — SQLAlchemy ORM models.
  3. Core tables: tenants, agents, agent_versions, runs, jobs, api_keys, usage_records.
  4. Principle P3: Works with both SQLite and PostgreSQL.
  5. """
  6. from __future__ import annotations
  7. import uuid
  8. import hashlib
  9. import json
  10. import time
  11. from datetime import datetime, timezone
  12. from typing import Optional, Dict, Any, List
  13. import sqlite3
  14. def now_utc():
  15. return datetime.now(timezone.utc).isoformat()
  16. def gen_id(prefix: str = "") -> str:
  17. return f"{prefix}{uuid.uuid4().hex[:12]}"
  18. class Database:
  19. """
  20. Lightweight database abstraction. Uses SQLite directly for MVP.
  21. Designed for easy migration to SQLAlchemy/PostgreSQL later.
  22. """
  23. def __init__(self, db_url: str = "sqlite:///./agentpaas.db"):
  24. if "sqlite" in db_url:
  25. db_path = db_url.replace("sqlite:///", "")
  26. self.conn = sqlite3.connect(db_path, check_same_thread=False)
  27. self.conn.row_factory = sqlite3.Row
  28. else:
  29. raise NotImplementedError("Only SQLite supported in MVP. Set AGENTPAAS_DATABASE_URL=sqlite:///./agentpaas.db")
  30. self._init_tables()
  31. def _init_tables(self):
  32. c = self.conn.cursor()
  33. c.executescript("""
  34. CREATE TABLE IF NOT EXISTS tenants (
  35. id TEXT PRIMARY KEY,
  36. name TEXT NOT NULL,
  37. plan TEXT DEFAULT 'free',
  38. quota_tokens_monthly INTEGER DEFAULT 1000000,
  39. quota_concurrency INTEGER DEFAULT 5,
  40. quota_agents INTEGER DEFAULT 20,
  41. status TEXT DEFAULT 'active',
  42. created_at TEXT
  43. );
  44. CREATE TABLE IF NOT EXISTS users (
  45. id TEXT PRIMARY KEY,
  46. tenant_id TEXT REFERENCES tenants(id),
  47. email TEXT,
  48. role TEXT DEFAULT 'developer',
  49. created_at TEXT
  50. );
  51. CREATE TABLE IF NOT EXISTS api_keys (
  52. id TEXT PRIMARY KEY,
  53. tenant_id TEXT REFERENCES tenants(id),
  54. user_id TEXT REFERENCES users(id),
  55. key_hash TEXT NOT NULL UNIQUE,
  56. key_prefix TEXT NOT NULL,
  57. name TEXT,
  58. scopes TEXT DEFAULT '[]',
  59. rate_limit INTEGER DEFAULT 60,
  60. expires_at TEXT,
  61. last_used_at TEXT,
  62. status TEXT DEFAULT 'active',
  63. created_at TEXT
  64. );
  65. CREATE TABLE IF NOT EXISTS agents (
  66. id TEXT PRIMARY KEY,
  67. tenant_id TEXT REFERENCES tenants(id),
  68. name TEXT NOT NULL,
  69. description TEXT DEFAULT '',
  70. current_version INTEGER DEFAULT 1,
  71. tags TEXT DEFAULT '[]',
  72. environment TEXT DEFAULT 'production',
  73. traffic_rules TEXT,
  74. status TEXT DEFAULT 'active',
  75. created_at TEXT,
  76. updated_at TEXT
  77. );
  78. CREATE TABLE IF NOT EXISTS agent_versions (
  79. agent_id TEXT REFERENCES agents(id),
  80. version INTEGER NOT NULL,
  81. config TEXT NOT NULL,
  82. config_hash TEXT NOT NULL,
  83. changelog TEXT DEFAULT '',
  84. created_by TEXT,
  85. created_at TEXT,
  86. PRIMARY KEY (agent_id, version)
  87. );
  88. CREATE TABLE IF NOT EXISTS runs (
  89. id TEXT PRIMARY KEY,
  90. agent_id TEXT REFERENCES agents(id),
  91. agent_version INTEGER,
  92. tenant_id TEXT REFERENCES tenants(id),
  93. job_id TEXT,
  94. input TEXT,
  95. output TEXT,
  96. status TEXT DEFAULT 'running',
  97. input_tokens INTEGER DEFAULT 0,
  98. output_tokens INTEGER DEFAULT 0,
  99. steps INTEGER DEFAULT 0,
  100. duration_ms INTEGER DEFAULT 0,
  101. error TEXT,
  102. trace_id TEXT,
  103. created_at TEXT,
  104. completed_at TEXT
  105. );
  106. CREATE TABLE IF NOT EXISTS jobs (
  107. id TEXT PRIMARY KEY,
  108. tenant_id TEXT REFERENCES tenants(id),
  109. agent_id TEXT REFERENCES agents(id),
  110. priority INTEGER DEFAULT 5,
  111. status TEXT DEFAULT 'queued',
  112. input TEXT,
  113. worker_id TEXT,
  114. attempts INTEGER DEFAULT 0,
  115. max_attempts INTEGER DEFAULT 3,
  116. started_at TEXT,
  117. completed_at TEXT,
  118. created_at TEXT
  119. );
  120. CREATE TABLE IF NOT EXISTS usage_records (
  121. id TEXT PRIMARY KEY,
  122. tenant_id TEXT REFERENCES tenants(id),
  123. agent_id TEXT REFERENCES agents(id),
  124. run_id TEXT REFERENCES runs(id),
  125. model TEXT,
  126. input_tokens INTEGER DEFAULT 0,
  127. output_tokens INTEGER DEFAULT 0,
  128. tool_calls INTEGER DEFAULT 0,
  129. duration_ms INTEGER DEFAULT 0,
  130. cost_usd REAL DEFAULT 0.0,
  131. created_at TEXT
  132. );
  133. """)
  134. self.conn.commit()
  135. def execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
  136. return self.conn.execute(sql, params)
  137. def commit(self):
  138. self.conn.commit()
  139. def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
  140. row = self.conn.execute(sql, params).fetchone()
  141. return dict(row) if row else None
  142. def fetchall(self, sql: str, params: tuple = ()) -> List[dict]:
  143. return [dict(r) for r in self.conn.execute(sql, params).fetchall()]