ya-agent-platform 0.58.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ya_agent_platform/__init__.py +4 -0
- ya_agent_platform/__main__.py +3 -0
- ya_agent_platform/alembic/env.py +71 -0
- ya_agent_platform/alembic/script.py.mako +27 -0
- ya_agent_platform/alembic/versions/__init__.py +1 -0
- ya_agent_platform/alembic.ini +44 -0
- ya_agent_platform/api/__init__.py +1 -0
- ya_agent_platform/api/health.py +39 -0
- ya_agent_platform/api/platform.py +61 -0
- ya_agent_platform/app.py +136 -0
- ya_agent_platform/cli.py +117 -0
- ya_agent_platform/config.py +43 -0
- ya_agent_platform/db/__init__.py +4 -0
- ya_agent_platform/db/base.py +7 -0
- ya_agent_platform/db/engine.py +25 -0
- ya_agent_platform/db/tables.py +9 -0
- ya_agent_platform/redis.py +7 -0
- ya_agent_platform-0.58.3.dist-info/METADATA +163 -0
- ya_agent_platform-0.58.3.dist-info/RECORD +21 -0
- ya_agent_platform-0.58.3.dist-info/WHEEL +4 -0
- ya_agent_platform-0.58.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from logging.config import fileConfig
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from alembic import context
|
|
7
|
+
from sqlalchemy import create_engine, pool
|
|
8
|
+
from ya_agent_platform.config import PlatformSettings
|
|
9
|
+
from ya_agent_platform.db import tables as _tables # noqa: F401
|
|
10
|
+
from ya_agent_platform.db.engine import to_sync_database_url
|
|
11
|
+
from ya_agent_platform.db.tables import Base
|
|
12
|
+
|
|
13
|
+
config = context.config
|
|
14
|
+
if config.config_file_name is not None:
|
|
15
|
+
fileConfig(config.config_file_name)
|
|
16
|
+
|
|
17
|
+
target_metadata = Base.metadata
|
|
18
|
+
|
|
19
|
+
settings = PlatformSettings()
|
|
20
|
+
if not settings.database_url:
|
|
21
|
+
msg = "YA_PLATFORM_DATABASE_URL is not set. Cannot run migrations."
|
|
22
|
+
raise RuntimeError(msg)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_url() -> str:
|
|
26
|
+
database_url = settings.database_url
|
|
27
|
+
if database_url is None:
|
|
28
|
+
msg = "database_url is None"
|
|
29
|
+
raise RuntimeError(msg)
|
|
30
|
+
return to_sync_database_url(database_url)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def include_object(obj: Any, name: str | None, type_: str, reflected: bool, compare_to: Any) -> bool:
|
|
34
|
+
return not (type_ == "table" and reflected and compare_to is None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_migrations_offline() -> None:
|
|
38
|
+
context.configure(
|
|
39
|
+
url=get_url(),
|
|
40
|
+
target_metadata=target_metadata,
|
|
41
|
+
include_object=include_object,
|
|
42
|
+
literal_binds=True,
|
|
43
|
+
dialect_opts={"paramstyle": "named"},
|
|
44
|
+
compare_type=True,
|
|
45
|
+
compare_server_default=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
with context.begin_transaction():
|
|
49
|
+
context.run_migrations()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_migrations_online() -> None:
|
|
53
|
+
connectable = create_engine(get_url(), poolclass=pool.NullPool)
|
|
54
|
+
|
|
55
|
+
with connectable.connect() as connection:
|
|
56
|
+
context.configure(
|
|
57
|
+
connection=connection,
|
|
58
|
+
target_metadata=target_metadata,
|
|
59
|
+
include_object=include_object,
|
|
60
|
+
compare_type=True,
|
|
61
|
+
compare_server_default=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
with context.begin_transaction():
|
|
65
|
+
context.run_migrations()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if context.is_offline_mode():
|
|
69
|
+
run_migrations_offline()
|
|
70
|
+
else:
|
|
71
|
+
run_migrations_online()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
${imports if imports else ""}
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision = ${repr(up_revision)}
|
|
17
|
+
down_revision = ${repr(down_revision)}
|
|
18
|
+
branch_labels = ${repr(branch_labels)}
|
|
19
|
+
depends_on = ${repr(depends_on)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Alembic migration versions for ya-agent-platform."""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Alembic configuration file.
|
|
2
|
+
# Production use: `ya-agent-platform migrate` or `ya-agent-platform db upgrade`.
|
|
3
|
+
|
|
4
|
+
[alembic]
|
|
5
|
+
script_location = %(here)s/alembic
|
|
6
|
+
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s_%%(slug)s
|
|
7
|
+
prepend_sys_path = .
|
|
8
|
+
path_separator = os
|
|
9
|
+
|
|
10
|
+
# Database URL is read from YA_PLATFORM_DATABASE_URL in env.py.
|
|
11
|
+
# Do not set sqlalchemy.url here.
|
|
12
|
+
|
|
13
|
+
[loggers]
|
|
14
|
+
keys = root,sqlalchemy,alembic
|
|
15
|
+
|
|
16
|
+
[handlers]
|
|
17
|
+
keys = console
|
|
18
|
+
|
|
19
|
+
[formatters]
|
|
20
|
+
keys = generic
|
|
21
|
+
|
|
22
|
+
[logger_root]
|
|
23
|
+
level = WARNING
|
|
24
|
+
handlers = console
|
|
25
|
+
|
|
26
|
+
[logger_sqlalchemy]
|
|
27
|
+
level = WARNING
|
|
28
|
+
handlers =
|
|
29
|
+
qualname = sqlalchemy.engine
|
|
30
|
+
|
|
31
|
+
[logger_alembic]
|
|
32
|
+
level = INFO
|
|
33
|
+
handlers =
|
|
34
|
+
qualname = alembic
|
|
35
|
+
|
|
36
|
+
[handler_console]
|
|
37
|
+
class = StreamHandler
|
|
38
|
+
args = (sys.stderr,)
|
|
39
|
+
level = NOTSET
|
|
40
|
+
formatter = generic
|
|
41
|
+
|
|
42
|
+
[formatter_generic]
|
|
43
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
44
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Request
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from sqlalchemy import text
|
|
6
|
+
|
|
7
|
+
router = APIRouter(tags=["health"])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HealthStatus(BaseModel):
|
|
11
|
+
status: str
|
|
12
|
+
postgres: str
|
|
13
|
+
redis: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/healthz", response_model=HealthStatus)
|
|
17
|
+
async def healthz(request: Request) -> HealthStatus:
|
|
18
|
+
postgres = "unavailable"
|
|
19
|
+
redis = "unavailable"
|
|
20
|
+
|
|
21
|
+
db_engine = getattr(request.app.state, "db_engine", None)
|
|
22
|
+
if db_engine is not None:
|
|
23
|
+
try:
|
|
24
|
+
async with db_engine.connect() as connection:
|
|
25
|
+
await connection.execute(text("SELECT 1"))
|
|
26
|
+
postgres = "ok"
|
|
27
|
+
except Exception:
|
|
28
|
+
postgres = "error"
|
|
29
|
+
|
|
30
|
+
redis_client = getattr(request.app.state, "redis", None)
|
|
31
|
+
if redis_client is not None:
|
|
32
|
+
try:
|
|
33
|
+
await redis_client.ping()
|
|
34
|
+
redis = "ok"
|
|
35
|
+
except Exception:
|
|
36
|
+
redis = "error"
|
|
37
|
+
|
|
38
|
+
status = "degraded" if "error" in {postgres, redis} else "ok"
|
|
39
|
+
return HealthStatus(status=status, postgres=postgres, redis=redis)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from ya_agent_platform.config import get_settings
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/api/v1/platform", tags=["platform"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlatformInfo(BaseModel):
|
|
12
|
+
name: str
|
|
13
|
+
environment: str
|
|
14
|
+
public_base_url: str
|
|
15
|
+
surfaces: list[str]
|
|
16
|
+
bridge_model: str
|
|
17
|
+
runtime_model: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PlatformTopology(BaseModel):
|
|
21
|
+
nodes: list[str]
|
|
22
|
+
edges: list[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/info", response_model=PlatformInfo)
|
|
26
|
+
async def platform_info() -> PlatformInfo:
|
|
27
|
+
settings = get_settings()
|
|
28
|
+
return PlatformInfo(
|
|
29
|
+
name=settings.app_name,
|
|
30
|
+
environment=settings.environment,
|
|
31
|
+
public_base_url=settings.public_base_url,
|
|
32
|
+
surfaces=["management-api", "chat-api", "bridge-api", "chat-ui"],
|
|
33
|
+
bridge_model="bridge adapters connect external IM systems to normalized platform events",
|
|
34
|
+
runtime_model="agent sessions run through ya-agent-sdk based runtimes and workers",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/topology", response_model=PlatformTopology)
|
|
39
|
+
async def platform_topology() -> PlatformTopology:
|
|
40
|
+
return PlatformTopology(
|
|
41
|
+
nodes=[
|
|
42
|
+
"chat-ui",
|
|
43
|
+
"management-api",
|
|
44
|
+
"chat-api",
|
|
45
|
+
"bridge-api",
|
|
46
|
+
"runtime-control",
|
|
47
|
+
"runtime-workers",
|
|
48
|
+
"postgres",
|
|
49
|
+
"redis-or-message-bus",
|
|
50
|
+
],
|
|
51
|
+
edges=[
|
|
52
|
+
"chat-ui -> management-api",
|
|
53
|
+
"chat-ui -> chat-api",
|
|
54
|
+
"bridge adapters -> bridge-api",
|
|
55
|
+
"management-api -> runtime-control",
|
|
56
|
+
"chat-api -> runtime-control",
|
|
57
|
+
"runtime-control -> runtime-workers",
|
|
58
|
+
"runtime-control -> postgres",
|
|
59
|
+
"runtime-control -> redis-or-message-bus",
|
|
60
|
+
],
|
|
61
|
+
)
|
ya_agent_platform/app.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, HTTPException
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
from fastapi.responses import FileResponse
|
|
10
|
+
|
|
11
|
+
from ya_agent_platform.api.health import router as health_router
|
|
12
|
+
from ya_agent_platform.api.platform import router as platform_router
|
|
13
|
+
from ya_agent_platform.config import PlatformSettings, get_settings
|
|
14
|
+
from ya_agent_platform.db.engine import create_engine
|
|
15
|
+
from ya_agent_platform.redis import create_redis_client
|
|
16
|
+
|
|
17
|
+
_RESERVED_FRONTEND_PATHS = ("api", "docs", "redoc", "openapi.json", "healthz")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@asynccontextmanager
|
|
21
|
+
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
22
|
+
settings = get_settings()
|
|
23
|
+
|
|
24
|
+
app.state.db_engine = None
|
|
25
|
+
app.state.redis = None
|
|
26
|
+
|
|
27
|
+
if settings.database_url:
|
|
28
|
+
app.state.db_engine = create_engine(
|
|
29
|
+
settings.database_url,
|
|
30
|
+
echo=settings.database_echo,
|
|
31
|
+
pool_size=settings.database_pool_size,
|
|
32
|
+
max_overflow=settings.database_max_overflow,
|
|
33
|
+
pool_recycle=settings.database_pool_recycle_seconds,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if settings.redis_url:
|
|
37
|
+
app.state.redis = create_redis_client(settings.redis_url)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
redis_client = app.state.redis
|
|
43
|
+
if redis_client is not None:
|
|
44
|
+
await redis_client.aclose()
|
|
45
|
+
|
|
46
|
+
db_engine = app.state.db_engine
|
|
47
|
+
if db_engine is not None:
|
|
48
|
+
await db_engine.dispose()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resolve_frontend_target(web_dist_dir: Path, requested_path: str) -> Path:
|
|
52
|
+
relative_path = requested_path.strip("/")
|
|
53
|
+
if relative_path == "":
|
|
54
|
+
return web_dist_dir / "index.html"
|
|
55
|
+
|
|
56
|
+
candidate = (web_dist_dir / relative_path).resolve()
|
|
57
|
+
web_root = web_dist_dir.resolve()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
candidate.relative_to(web_root)
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise HTTPException(status_code=404, detail="Invalid frontend asset path.") from exc
|
|
63
|
+
|
|
64
|
+
if candidate.is_file():
|
|
65
|
+
return candidate
|
|
66
|
+
|
|
67
|
+
return web_dist_dir / "index.html"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _register_frontend(app: FastAPI, settings: PlatformSettings) -> bool:
|
|
71
|
+
web_dist_dir = settings.web_dist_dir
|
|
72
|
+
if web_dist_dir is None:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
index_file = web_dist_dir / "index.html"
|
|
76
|
+
if not web_dist_dir.exists() or not index_file.exists():
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
@app.get("/", include_in_schema=False)
|
|
80
|
+
async def frontend_index() -> FileResponse:
|
|
81
|
+
return FileResponse(index_file)
|
|
82
|
+
|
|
83
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
84
|
+
async def frontend_route(full_path: str) -> FileResponse:
|
|
85
|
+
normalized_path = full_path.strip("/")
|
|
86
|
+
if normalized_path in _RESERVED_FRONTEND_PATHS or any(
|
|
87
|
+
normalized_path.startswith(f"{prefix}/") for prefix in _RESERVED_FRONTEND_PATHS if "/" not in prefix
|
|
88
|
+
):
|
|
89
|
+
raise HTTPException(status_code=404, detail="Route not found.")
|
|
90
|
+
|
|
91
|
+
return FileResponse(_resolve_frontend_target(web_dist_dir, normalized_path))
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_app() -> FastAPI:
|
|
97
|
+
settings = get_settings()
|
|
98
|
+
|
|
99
|
+
app = FastAPI(
|
|
100
|
+
title=settings.app_name,
|
|
101
|
+
version="0.1.0",
|
|
102
|
+
description="Cloud-ready agent platform backend built on top of ya-agent-sdk.",
|
|
103
|
+
lifespan=_lifespan,
|
|
104
|
+
)
|
|
105
|
+
app.state.settings = settings
|
|
106
|
+
|
|
107
|
+
app.add_middleware(
|
|
108
|
+
CORSMiddleware,
|
|
109
|
+
allow_origins=settings.allow_origins,
|
|
110
|
+
allow_credentials=True,
|
|
111
|
+
allow_methods=["*"],
|
|
112
|
+
allow_headers=["*"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
app.include_router(health_router)
|
|
116
|
+
app.include_router(platform_router)
|
|
117
|
+
|
|
118
|
+
frontend_registered = _register_frontend(app, settings)
|
|
119
|
+
|
|
120
|
+
if not frontend_registered:
|
|
121
|
+
|
|
122
|
+
@app.get("/")
|
|
123
|
+
async def index() -> dict[str, object]:
|
|
124
|
+
return {
|
|
125
|
+
"name": settings.app_name,
|
|
126
|
+
"environment": settings.environment,
|
|
127
|
+
"docs_url": "/docs",
|
|
128
|
+
"spec_path": "packages/ya-agent-platform/spec",
|
|
129
|
+
"surfaces": {
|
|
130
|
+
"admin": settings.admin_mount_path,
|
|
131
|
+
"chat": settings.chat_mount_path,
|
|
132
|
+
"bridges": settings.bridge_mount_path,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return app
|
ya_agent_platform/cli.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
from ya_agent_platform.config import get_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cli() -> None:
|
|
13
|
+
"""YA Agent Platform management CLI."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _alembic_config():
|
|
17
|
+
"""Build an Alembic Config from the package's alembic.ini."""
|
|
18
|
+
from alembic.config import Config
|
|
19
|
+
|
|
20
|
+
ini_path = Path(__file__).parent / "alembic.ini"
|
|
21
|
+
return Config(str(ini_path))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_database_url() -> str:
|
|
25
|
+
settings = get_settings()
|
|
26
|
+
if settings.database_url:
|
|
27
|
+
return settings.database_url
|
|
28
|
+
|
|
29
|
+
click.echo("Error: YA_PLATFORM_DATABASE_URL is required.", err=True)
|
|
30
|
+
raise SystemExit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _apply_database_migrations(revision: str = "head") -> None:
|
|
34
|
+
from alembic import command
|
|
35
|
+
|
|
36
|
+
_ensure_database_url()
|
|
37
|
+
command.upgrade(_alembic_config(), revision)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@cli.command("serve")
|
|
41
|
+
@click.option("--host", default=None, help="Bind host for the HTTP server.")
|
|
42
|
+
@click.option("--port", default=None, type=int, help="Bind port for the HTTP server.")
|
|
43
|
+
@click.option("--reload/--no-reload", default=None, help="Enable or disable code reload.")
|
|
44
|
+
@click.option("--migrate/--no-migrate", default=None, help="Run database migrations before starting the server.")
|
|
45
|
+
def serve(host: str | None, port: int | None, reload: bool | None, migrate: bool | None) -> None:
|
|
46
|
+
settings = get_settings()
|
|
47
|
+
resolved_host = host or settings.host
|
|
48
|
+
resolved_port = port or settings.port
|
|
49
|
+
resolved_reload = settings.reload if reload is None else reload
|
|
50
|
+
resolved_migrate = settings.auto_migrate if migrate is None else migrate
|
|
51
|
+
|
|
52
|
+
if resolved_migrate and settings.database_url:
|
|
53
|
+
_apply_database_migrations()
|
|
54
|
+
click.echo("Database migrations applied.")
|
|
55
|
+
|
|
56
|
+
uvicorn.run(
|
|
57
|
+
"ya_agent_platform.app:create_app",
|
|
58
|
+
factory=True,
|
|
59
|
+
host=resolved_host,
|
|
60
|
+
port=resolved_port,
|
|
61
|
+
reload=resolved_reload,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@cli.command("migrate")
|
|
66
|
+
@click.option("--revision", default="head", help="Target revision for the migration run.")
|
|
67
|
+
def migrate_command(revision: str) -> None:
|
|
68
|
+
_apply_database_migrations(revision)
|
|
69
|
+
click.echo(f"Database upgraded to {revision}.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cli.group()
|
|
73
|
+
def db() -> None:
|
|
74
|
+
"""Database migration and management commands."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@db.command()
|
|
78
|
+
@click.option("--revision", default="head", help="Target revision (default: head).")
|
|
79
|
+
def upgrade(revision: str) -> None:
|
|
80
|
+
_apply_database_migrations(revision)
|
|
81
|
+
click.echo(f"Database upgraded to {revision}.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@db.command()
|
|
85
|
+
@click.option("--revision", default="-1", help="Target revision (default: -1, one step back).")
|
|
86
|
+
def downgrade(revision: str) -> None:
|
|
87
|
+
from alembic import command
|
|
88
|
+
|
|
89
|
+
_ensure_database_url()
|
|
90
|
+
command.downgrade(_alembic_config(), revision)
|
|
91
|
+
click.echo(f"Database downgraded to {revision}.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@db.command("migrate")
|
|
95
|
+
@click.argument("message")
|
|
96
|
+
def create_migration(message: str) -> None:
|
|
97
|
+
from alembic import command
|
|
98
|
+
|
|
99
|
+
_ensure_database_url()
|
|
100
|
+
command.revision(_alembic_config(), message=message, autogenerate=True)
|
|
101
|
+
click.echo(f"Migration generated: {message}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@db.command()
|
|
105
|
+
def current() -> None:
|
|
106
|
+
from alembic import command
|
|
107
|
+
|
|
108
|
+
_ensure_database_url()
|
|
109
|
+
command.current(_alembic_config(), verbose=True)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@db.command()
|
|
113
|
+
def history() -> None:
|
|
114
|
+
from alembic import command
|
|
115
|
+
|
|
116
|
+
_ensure_database_url()
|
|
117
|
+
command.history(_alembic_config(), verbose=True)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlatformSettings(BaseSettings):
|
|
11
|
+
model_config = SettingsConfigDict(
|
|
12
|
+
env_prefix="YA_PLATFORM_",
|
|
13
|
+
env_file=".env",
|
|
14
|
+
env_file_encoding="utf-8",
|
|
15
|
+
extra="ignore",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app_name: str = "YA Agent Platform"
|
|
19
|
+
environment: str = "development"
|
|
20
|
+
host: str = "127.0.0.1"
|
|
21
|
+
port: int = 9042
|
|
22
|
+
reload: bool = False
|
|
23
|
+
public_base_url: str = "http://127.0.0.1:9042"
|
|
24
|
+
admin_mount_path: str = "/admin"
|
|
25
|
+
chat_mount_path: str = "/chat"
|
|
26
|
+
bridge_mount_path: str = "/bridges"
|
|
27
|
+
web_dist_dir: Path | None = None
|
|
28
|
+
allow_origins: list[str] = Field(default_factory=lambda: ["http://127.0.0.1:5173", "http://localhost:5173"])
|
|
29
|
+
|
|
30
|
+
database_url: str | None = None
|
|
31
|
+
database_echo: bool = False
|
|
32
|
+
database_pool_size: int = 5
|
|
33
|
+
database_max_overflow: int = 10
|
|
34
|
+
database_pool_recycle_seconds: int = 3600
|
|
35
|
+
|
|
36
|
+
redis_url: str | None = None
|
|
37
|
+
|
|
38
|
+
auto_migrate: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@lru_cache(maxsize=1)
|
|
42
|
+
def get_settings() -> PlatformSettings:
|
|
43
|
+
return PlatformSettings()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_engine(database_url: str, **kwargs: object) -> AsyncEngine:
|
|
7
|
+
defaults: dict[str, object] = {
|
|
8
|
+
"echo": False,
|
|
9
|
+
"pool_size": 5,
|
|
10
|
+
"max_overflow": 10,
|
|
11
|
+
"pool_pre_ping": True,
|
|
12
|
+
"pool_recycle": 3600,
|
|
13
|
+
}
|
|
14
|
+
defaults.update(kwargs)
|
|
15
|
+
return create_async_engine(database_url, **defaults)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
|
|
19
|
+
return async_sessionmaker(engine, expire_on_commit=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_sync_database_url(database_url: str) -> str:
|
|
23
|
+
return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg://").replace(
|
|
24
|
+
"postgresql+psycopg_async://", "postgresql+psycopg://"
|
|
25
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ya-agent-platform
|
|
3
|
+
Version: 0.58.3
|
|
4
|
+
Summary: Cloud-ready agent platform built on top of ya-agent-sdk
|
|
5
|
+
Project-URL: Repository, https://github.com/wh1isper/ya-mono
|
|
6
|
+
Author-email: wh1isper <jizhongsheng957@gmail.com>
|
|
7
|
+
Keywords: agent-platform,ai-agent,fastapi,python
|
|
8
|
+
Classifier: Framework :: FastAPI
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: <3.14,>=3.11
|
|
17
|
+
Requires-Dist: alembic>=1.16.0
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Requires-Dist: fastapi>=0.116.0
|
|
20
|
+
Requires-Dist: psycopg[binary]>=3.2.0
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
22
|
+
Requires-Dist: pydantic>=2.12.0
|
|
23
|
+
Requires-Dist: redis[hiredis]>=6.0.0
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
25
|
+
Requires-Dist: sse-starlette>=3.0.0
|
|
26
|
+
Requires-Dist: uvicorn[standard]>=0.35.0
|
|
27
|
+
Requires-Dist: ya-agent-sdk[all]==0.58.3
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# YA Agent Platform
|
|
31
|
+
|
|
32
|
+
Cloud-ready agent platform package for the `ya-mono` workspace.
|
|
33
|
+
|
|
34
|
+
## Scope
|
|
35
|
+
|
|
36
|
+
This package initializes the backend service for a complete agent platform:
|
|
37
|
+
|
|
38
|
+
- management API for platform and workspace administration
|
|
39
|
+
- chat-facing API for first-party Chat UI
|
|
40
|
+
- bridge-facing API surface for IM connectors
|
|
41
|
+
- runtime integration points for `ya-agent-sdk`
|
|
42
|
+
- persistence scaffold with PostgreSQL, Redis, packaged Alembic migrations, and startup auto-migration
|
|
43
|
+
- specification documents that define the target architecture before full implementation
|
|
44
|
+
|
|
45
|
+
## Current Layout
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
packages/ya-agent-platform/
|
|
49
|
+
├── README.md
|
|
50
|
+
├── infra/
|
|
51
|
+
│ ├── dev.env
|
|
52
|
+
│ └── docker-compose.dev.yml
|
|
53
|
+
├── pyproject.toml
|
|
54
|
+
├── spec/
|
|
55
|
+
├── start.sh
|
|
56
|
+
├── tests/
|
|
57
|
+
└── ya_agent_platform/
|
|
58
|
+
├── alembic/
|
|
59
|
+
├── alembic.ini
|
|
60
|
+
├── api/
|
|
61
|
+
├── app.py
|
|
62
|
+
├── cli.py
|
|
63
|
+
├── config.py
|
|
64
|
+
├── db/
|
|
65
|
+
└── redis.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
From the workspace root:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync --all-packages
|
|
74
|
+
make platform-infra-up
|
|
75
|
+
set -a && source packages/ya-agent-platform/infra/dev.env && set +a
|
|
76
|
+
uv run --package ya-agent-platform ya-agent-platform serve --reload
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The development server listens on `http://127.0.0.1:9042` by default.
|
|
80
|
+
|
|
81
|
+
## Database and Redis Commands
|
|
82
|
+
|
|
83
|
+
Use the package CLI directly:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run --package ya-agent-platform ya-agent-platform migrate
|
|
87
|
+
uv run --package ya-agent-platform ya-agent-platform db current
|
|
88
|
+
uv run --package ya-agent-platform ya-agent-platform db history
|
|
89
|
+
uv run --package ya-agent-platform ya-agent-platform db migrate "add workspace tables"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use the workspace Makefile wrappers:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
make platform-db-upgrade
|
|
96
|
+
make platform-db-current
|
|
97
|
+
make platform-db-history
|
|
98
|
+
make platform-db-migrate MSG="add workspace tables"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Auto Migration
|
|
102
|
+
|
|
103
|
+
`YA_PLATFORM_AUTO_MIGRATE=true` is the default behavior.
|
|
104
|
+
|
|
105
|
+
- `ya-agent-platform serve` applies migrations before boot when `YA_PLATFORM_DATABASE_URL` is configured
|
|
106
|
+
- `ya-agent-platform migrate` runs migrations separately
|
|
107
|
+
- `start.sh` applies migrations before starting the server in container environments
|
|
108
|
+
|
|
109
|
+
## Development Infrastructure
|
|
110
|
+
|
|
111
|
+
The dev compose file starts PostgreSQL and Redis:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
make platform-infra-up
|
|
115
|
+
make platform-infra-status
|
|
116
|
+
make platform-infra-down
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Default development URLs live in `packages/ya-agent-platform/infra/dev.env`.
|
|
120
|
+
|
|
121
|
+
## Combined Docker Image
|
|
122
|
+
|
|
123
|
+
The repository root `Dockerfile` builds a single production image that contains:
|
|
124
|
+
|
|
125
|
+
- the `ya-agent-platform` backend
|
|
126
|
+
- the bundled `ya-agent-platform-web` frontend
|
|
127
|
+
- FastAPI static serving for the built web assets
|
|
128
|
+
- startup auto-migration support through `packages/ya-agent-platform/start.sh`
|
|
129
|
+
|
|
130
|
+
Build locally from the repository root:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker build -t ya-agent-platform:dev .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Run locally:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
docker run --rm -p 9042:9042 ya-agent-platform:dev
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The container serves the combined application on `http://127.0.0.1:9042`.
|
|
143
|
+
|
|
144
|
+
## Initial API Surface
|
|
145
|
+
|
|
146
|
+
- `GET /healthz` — service health probe with postgres and redis component status
|
|
147
|
+
- `GET /api/v1/platform/info` — platform metadata and enabled surfaces
|
|
148
|
+
- `GET /api/v1/platform/topology` — high-level component topology for the UI and tooling
|
|
149
|
+
|
|
150
|
+
## Specification Set
|
|
151
|
+
|
|
152
|
+
- [`spec/README.md`](spec/README.md)
|
|
153
|
+
- [`spec/000-platform-overview.md`](spec/000-platform-overview.md)
|
|
154
|
+
- [`spec/001-system-architecture.md`](spec/001-system-architecture.md)
|
|
155
|
+
- [`spec/002-bridge-contract.md`](spec/002-bridge-contract.md)
|
|
156
|
+
- [`spec/003-http-api.md`](spec/003-http-api.md)
|
|
157
|
+
|
|
158
|
+
## Next Build Phase
|
|
159
|
+
|
|
160
|
+
1. add persistence models and first migrations
|
|
161
|
+
2. add runtime orchestration and worker execution
|
|
162
|
+
3. add bridge registry and delivery guarantees
|
|
163
|
+
4. connect the web app to live platform endpoints
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ya_agent_platform/__init__.py,sha256=13C_Udthyw4k6eSBy2nsqqlNRjJAvM16pdqct27E7uA,175
|
|
2
|
+
ya_agent_platform/__main__.py,sha256=2502gK30qurdBEUi2cVKrq5CVAcEGlr7x99dxnoMaew,45
|
|
3
|
+
ya_agent_platform/alembic.ini,sha256=2hXTb5zVfDNd91n5DXfO-DArBWnClJp5i3zIWZivn_c,837
|
|
4
|
+
ya_agent_platform/app.py,sha256=racy4giHeBcFvngCfh-5n34cUYe9c4kghlfhDOeJ6i0,4261
|
|
5
|
+
ya_agent_platform/cli.py,sha256=PWBzu6C5aO6vfOgeZW4esj7qG8eP2f2rQzkPX8bf9d4,3384
|
|
6
|
+
ya_agent_platform/config.py,sha256=FcstDVY3q4gvXXRV5HG-AqJMtMaa1DQwAG0_YcJuraU,1195
|
|
7
|
+
ya_agent_platform/redis.py,sha256=FbX9kghVVh-DpWXDbburyB9m-rTX_KX4C2EBXUEsuBk,238
|
|
8
|
+
ya_agent_platform/alembic/env.py,sha256=z1HUHsCR2A1gk6-g_9TuSJmVHPEdFQYv0aGka0ghE4Q,2020
|
|
9
|
+
ya_agent_platform/alembic/script.py.mako,sha256=LIKpNreaIjWwfG8LSVF9TBkGT4av_lhzQ-yBfo9gYXI,547
|
|
10
|
+
ya_agent_platform/alembic/versions/__init__.py,sha256=CyNpGB0vaUHVDQg2asrD2P5ZfuELZtyHr9J4upBBnOY,56
|
|
11
|
+
ya_agent_platform/api/__init__.py,sha256=da1PTClDMl-IBkrSvq6JC1lnS-K_BASzCvxVhNxN5Ls,13
|
|
12
|
+
ya_agent_platform/api/health.py,sha256=bLZO6l0njKYS1itBjni7fmCjHSpMLzU2xNCyHCk7LKs,1087
|
|
13
|
+
ya_agent_platform/api/platform.py,sha256=3EmgGv1GVqaJW5LFn1XhUSwo1uQKMtfb0ywk4RFhhEo,1785
|
|
14
|
+
ya_agent_platform/db/__init__.py,sha256=beJAosi9jEi_zYoVImwNwOvkFntdfE3ukD1jpF2QwdY,230
|
|
15
|
+
ya_agent_platform/db/base.py,sha256=Fp7n4EfxQOxzqm34-D-rYyu60P88eZUIewCBNP_Iuc4,119
|
|
16
|
+
ya_agent_platform/db/engine.py,sha256=eTt5ofSk3WYTTJAd5qL1OBY9ghbc0NyhNW87JKSCVFo,837
|
|
17
|
+
ya_agent_platform/db/tables.py,sha256=r6gFpJkol_pvWsHag9tP_gKm_6cWUFFnV6egf5elvyA,234
|
|
18
|
+
ya_agent_platform-0.58.3.dist-info/METADATA,sha256=Mp291pGwIWR8oR-fKJtlF5eoNB1C1W5-9PuqHjckYuw,4916
|
|
19
|
+
ya_agent_platform-0.58.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
20
|
+
ya_agent_platform-0.58.3.dist-info/entry_points.txt,sha256=eN6WvrbjfNf7kIBWJ8i1faCS24ZPf73ZRnX4VxxBszA,64
|
|
21
|
+
ya_agent_platform-0.58.3.dist-info/RECORD,,
|