瀏覽代碼

feat: add team service

Jax Docker 1 月之前
父節點
當前提交
ff4d7443b6
共有 32 個文件被更改,包括 1113 次插入0 次删除
  1. 37 0
      README.md
  2. 27 0
      deployments/docker/docker-compose.yml
  3. 18 0
      libs/core-domain/src/core_domain/__init__.py
  4. 69 0
      libs/core-domain/src/core_domain/team_contracts.py
  5. 1 0
      pyproject.toml
  6. 27 0
      services/api-gateway/app/api/routes.py
  7. 1 0
      services/api-gateway/app/bootstrap/settings.py
  8. 1 0
      services/api-gateway/app/infrastructure/proxy.py
  9. 36 0
      services/team-service/alembic.ini
  10. 41 0
      services/team-service/alembic/env.py
  11. 1 0
      services/team-service/alembic/versions/.gitkeep
  12. 118 0
      services/team-service/alembic/versions/20260425_0001_init_team_models.py
  13. 1 0
      services/team-service/app/__init__.py
  14. 1 0
      services/team-service/app/api/__init__.py
  15. 133 0
      services/team-service/app/api/routes.py
  16. 1 0
      services/team-service/app/application/__init__.py
  17. 142 0
      services/team-service/app/application/services.py
  18. 1 0
      services/team-service/app/bootstrap/__init__.py
  19. 17 0
      services/team-service/app/bootstrap/app.py
  20. 7 0
      services/team-service/app/bootstrap/settings.py
  21. 1 0
      services/team-service/app/db/__init__.py
  22. 7 0
      services/team-service/app/db/models/__init__.py
  23. 18 0
      services/team-service/app/db/models/team_definition.py
  24. 28 0
      services/team-service/app/db/models/team_run.py
  25. 21 0
      services/team-service/app/db/models/team_version.py
  26. 27 0
      services/team-service/app/db/session.py
  27. 1 0
      services/team-service/app/domain/__init__.py
  28. 222 0
      services/team-service/app/domain/repositories.py
  29. 3 0
      services/team-service/app/main.py
  30. 1 0
      services/team-service/app/schemas/__init__.py
  31. 79 0
      services/team-service/app/schemas/team.py
  32. 25 0
      services/team-service/pyproject.toml

+ 37 - 0
README.md

@@ -18,6 +18,7 @@
 - `runtime-service`
 - `agent-service`
 - `memory-service`
+- `team-service`
 - `tool-service`
 
 每个服务都提供了最小 `FastAPI` 启动入口和健康检查接口,数据库相关服务也已经带上了 `SQLAlchemy` 模型骨架与 Alembic 目录。
@@ -49,6 +50,7 @@ pip install -e .\services\workflow-service
 pip install -e .\services\runtime-service
 pip install -e .\services\agent-service
 pip install -e .\services\memory-service
+pip install -e .\services\team-service
 pip install -e .\services\tool-service
 ```
 
@@ -273,6 +275,39 @@ Invoke-RestMethod -Method Post `
 
 Through `api-gateway`, use `/gateway/memories/**`.
 
+## Team Service APIs
+
+`team-service` stores multi-agent team definitions, versioned member composition, coordination mode, and team run records. The first version provides the team management backbone; later versions can connect team runs to supervisor/planner/member agent execution.
+
+Create a team:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8009/teams `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","code":"research_team","name":"Research Team","team_type":"collaborative"}'
+```
+
+Create a published team version:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8009/teams/versions `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","team_id":"team-id","status":"published","coordination_mode":"supervisor","objective":"Research and summarize complex questions","member_refs":[{"member_key":"lead","agent_id":"agent-lead","role":"supervisor","responsibility":"Plan and assign work"},{"member_key":"writer","agent_id":"agent-writer","role":"executor","responsibility":"Draft final answer"}]}'
+```
+
+Create a team run. If `team_version_id` is omitted, the latest published version is used:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8009/teams/runs `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","team_id":"team-id","session_id":"session-id","input_text":"Analyze this customer request."}'
+```
+
+Through `api-gateway`, use `/gateway/teams/**`.
+
 Execute an agent run without calling an external model:
 
 ```powershell
@@ -566,6 +601,7 @@ $env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
 - `/gateway/runtime/**` -> `runtime-service /runtime/**`
 - `/gateway/agents/**` -> `agent-service /agents/**`
 - `/gateway/memories/**` -> `memory-service /memories/**`
+- `/gateway/teams/**` -> `team-service /teams/**`
 - `/gateway/tools/**` -> `tool-service /tools/**`
 - `/gateway/models/**` -> `model-gateway-service /models/**`
 - `/gateway/code/**` -> `code-runner-service /code/**`
@@ -799,6 +835,7 @@ Important notes:
 - `workflow-service`, `session-service`, `runtime-service`, `tool-service`, and `api-gateway` use SQLite files mounted under `/data`
 - `agent-service` stores agent definitions, prompt/config versions, and agent run records under `/data`
 - `memory-service` stores scoped memories under `/data`; move it to PostgreSQL before enabling high-volume memory writes
+- `team-service` stores multi-agent team definitions, team versions, and team run records under `/data`
 - `agent-worker` has no exposed port and can be scaled independently; set `AGENT_PLATFORM_AGENT_WORKER_DRY_RUN=true` for no-key local smoke runs
 - `runtime-worker` has no exposed port and can be scaled independently; prefer PostgreSQL for real multi-worker write concurrency
 - `runtime-service` automatically resolves internal URLs to `workflow-service`, `tool-service`, `model-gateway-service`, and `code-runner-service`

+ 27 - 0
deployments/docker/docker-compose.yml

@@ -171,6 +171,26 @@ services:
       timeout: 5s
       retries: 5
 
+  team-service:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/team-service
+    container_name: agent-platform-team-service
+    command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8009"]
+    environment:
+      AGENT_PLATFORM_DATABASE_URL: sqlite:////data/team_service.db
+    ports:
+      - "8009:8009"
+    volumes:
+      - team_service_data:/data
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8009/teams/health').read()"]
+      interval: 15s
+      timeout: 5s
+      retries: 5
+
   runtime-service:
     build:
       context: ../..
@@ -187,6 +207,7 @@ services:
       AGENT_PLATFORM_CODE_RUNNER_SERVICE_URL: http://code-runner-service:8006
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
       AGENT_PLATFORM_MEMORY_SERVICE_URL: http://memory-service:8008
+      AGENT_PLATFORM_TEAM_SERVICE_URL: http://team-service:8009
     ports:
       - "8003:8003"
     volumes:
@@ -204,6 +225,8 @@ services:
         condition: service_started
       memory-service:
         condition: service_started
+      team-service:
+        condition: service_started
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8003/runtime/health').read()"]
       interval: 15s
@@ -255,6 +278,7 @@ services:
       AGENT_PLATFORM_CODE_RUNNER_SERVICE_URL: http://code-runner-service:8006
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
       AGENT_PLATFORM_MEMORY_SERVICE_URL: http://memory-service:8008
+      AGENT_PLATFORM_TEAM_SERVICE_URL: http://team-service:8009
       AGENT_PLATFORM_AUTH_REQUIRED: ${AGENT_PLATFORM_AUTH_REQUIRED:-false}
     ports:
       - "8000:8000"
@@ -277,6 +301,8 @@ services:
         condition: service_started
       memory-service:
         condition: service_started
+      team-service:
+        condition: service_started
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health').read()"]
       interval: 15s
@@ -287,6 +313,7 @@ volumes:
   api_gateway_data:
   agent_service_data:
   memory_service_data:
+  team_service_data:
   workflow_service_data:
   session_service_data:
   runtime_service_data:

+ 18 - 0
libs/core-domain/src/core_domain/__init__.py

@@ -42,6 +42,16 @@ from .runtime_contracts import (
     WorkflowRunContract,
 )
 from .service import ServiceDescriptor, ServiceHealth
+from .team_contracts import (
+    TeamDefinitionContract,
+    TeamMemberContract,
+    TeamMemberRole,
+    TeamRunContract,
+    TeamRunStatus,
+    TeamStatus,
+    TeamVersionContract,
+    TeamVersionStatus,
+)
 from .tool_contracts import (
     ToolBindingContract,
     ToolBindingDetailContract,
@@ -84,6 +94,14 @@ __all__ = [
     "RunCreateContract",
     "ServiceDescriptor",
     "ServiceHealth",
+    "TeamDefinitionContract",
+    "TeamMemberContract",
+    "TeamMemberRole",
+    "TeamRunContract",
+    "TeamRunStatus",
+    "TeamStatus",
+    "TeamVersionContract",
+    "TeamVersionStatus",
     "ToolBindingContract",
     "ToolBindingDetailContract",
     "ToolDefinitionContract",

+ 69 - 0
libs/core-domain/src/core_domain/team_contracts.py

@@ -0,0 +1,69 @@
+from datetime import datetime
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+from core_shared import JSONValue
+
+
+TeamStatus = Literal["draft", "active", "archived"]
+TeamVersionStatus = Literal["draft", "published", "deprecated"]
+TeamRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
+TeamMemberRole = Literal["supervisor", "planner", "executor", "reviewer", "specialist"]
+
+
+class TeamMemberContract(BaseModel):
+    member_key: str
+    agent_id: str
+    agent_version_id: str | None = None
+    role: TeamMemberRole = "specialist"
+    name: str | None = None
+    responsibility: str | None = None
+    config_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class TeamDefinitionContract(BaseModel):
+    id: str
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    team_type: str
+    status: TeamStatus
+    owner_user_id: str | None = None
+    created_time: datetime
+
+
+class TeamVersionContract(BaseModel):
+    id: str
+    tenant_id: str
+    team_id: str
+    version_no: int
+    status: TeamVersionStatus
+    coordination_mode: str
+    objective: str | None = None
+    member_refs_json: list[dict[str, JSONValue]]
+    policy_json: dict[str, JSONValue]
+    published_time: datetime | None = None
+    created_time: datetime
+
+
+class TeamRunContract(BaseModel):
+    id: str
+    tenant_id: str
+    team_id: str
+    team_version_id: str
+    session_id: str | None = None
+    input_text: str | None = None
+    input_json: dict[str, JSONValue] | None = None
+    output_text: str | None = None
+    output_json: dict[str, JSONValue] | None = None
+    status: TeamRunStatus
+    worker_key: str | None = None
+    queued_time: datetime | None = None
+    lease_expire_time: datetime | None = None
+    started_time: datetime | None = None
+    finished_time: datetime | None = None
+    error_code: str | None = None
+    error_message: str | None = None
+    created_time: datetime

+ 1 - 0
pyproject.toml

@@ -11,6 +11,7 @@ members = [
   "services/memory-service",
   "services/model-gateway-service",
   "services/session-service",
+  "services/team-service",
   "services/workflow-service",
   "services/runtime-service",
   "services/tool-service",

+ 27 - 0
services/api-gateway/app/api/routes.py

@@ -163,6 +163,12 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             path_prefix="/memories",
             health_path="/memories/health",
         ),
+        "team-service": ProxyTarget(
+            service_name="team-service",
+            base_url=settings.team_service_url,
+            path_prefix="/teams",
+            health_path="/teams/health",
+        ),
     }
 
 
@@ -287,6 +293,27 @@ async def proxy_memory_service(
     )
 
 
+@router.api_route(
+    "/gateway/teams",
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
+)
+@router.api_route(
+    "/gateway/teams/{path:path}",
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
+)
+async def proxy_team_service(
+    request: Request,
+    path: str = "",
+    settings: ApiGatewaySettings = Depends(get_gateway_settings),
+    proxy: ServiceProxy = Depends(get_service_proxy),
+) -> Response:
+    return await proxy.forward(
+        request=request,
+        target=build_proxy_targets(settings)["team-service"],
+        path=path,
+    )
+
+
 @router.api_route(
     "/gateway/tools",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"],

+ 1 - 0
services/api-gateway/app/bootstrap/settings.py

@@ -13,6 +13,7 @@ class ApiGatewaySettings(ServiceSettings):
     code_runner_service_url: str = "http://127.0.0.1:8006"
     agent_service_url: str = "http://127.0.0.1:8007"
     memory_service_url: str = "http://127.0.0.1:8008"
+    team_service_url: str = "http://127.0.0.1:8009"
     proxy_timeout_seconds: float = 30.0
     downstream_health_timeout_seconds: float = 2.0
     auth_required: bool = False

+ 1 - 0
services/api-gateway/app/infrastructure/proxy.py

@@ -17,6 +17,7 @@ ProxyServiceName = Literal[
     "code-runner-service",
     "agent-service",
     "memory-service",
+    "team-service",
 ]
 
 

+ 36 - 0
services/team-service/alembic.ini

@@ -0,0 +1,36 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./team_service.db
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers = console
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s

+ 41 - 0
services/team-service/alembic/env.py

@@ -0,0 +1,41 @@
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+from app.db.models import Base
+
+config = context.config
+
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section, {}),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 1 - 0
services/team-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 118 - 0
services/team-service/alembic/versions/20260425_0001_init_team_models.py

@@ -0,0 +1,118 @@
+"""init team models
+
+Revision ID: 20260425_0001
+Revises:
+Create Date: 2026-04-25 14:10:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260425_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "team_definition",
+        sa.Column("code", sa.String(length=64), nullable=False),
+        sa.Column("name", sa.String(length=128), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("team_type", sa.String(length=32), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("owner_user_id", sa.String(length=36), nullable=True),
+        sa.Column("metadata_json", sa.JSON(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_team_definition_code", "team_definition", ["code"], unique=False)
+    op.create_index("ix_team_definition_status", "team_definition", ["status"], unique=False)
+    op.create_index("ix_team_definition_tenant_id", "team_definition", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "team_version",
+        sa.Column("team_id", sa.String(length=36), nullable=False),
+        sa.Column("version_no", sa.Integer(), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("coordination_mode", sa.String(length=64), nullable=False),
+        sa.Column("objective", sa.Text(), nullable=True),
+        sa.Column("member_refs_json", sa.JSON(), nullable=False),
+        sa.Column("policy_json", sa.JSON(), nullable=False),
+        sa.Column("published_time", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_team_version_team_id", "team_version", ["team_id"], unique=False)
+    op.create_index("ix_team_version_status", "team_version", ["status"], unique=False)
+    op.create_index("ix_team_version_tenant_id", "team_version", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "team_run",
+        sa.Column("team_id", sa.String(length=36), nullable=False),
+        sa.Column("team_version_id", sa.String(length=36), nullable=False),
+        sa.Column("session_id", sa.String(length=36), nullable=True),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("worker_key", sa.String(length=128), nullable=True),
+        sa.Column("queued_time", sa.DateTime(), nullable=True),
+        sa.Column("lease_expire_time", sa.DateTime(), nullable=True),
+        sa.Column("input_text", sa.Text(), nullable=True),
+        sa.Column("input_json", sa.JSON(), nullable=True),
+        sa.Column("output_text", sa.Text(), nullable=True),
+        sa.Column("output_json", sa.JSON(), nullable=True),
+        sa.Column("started_time", sa.DateTime(), nullable=True),
+        sa.Column("finished_time", sa.DateTime(), nullable=True),
+        sa.Column("error_code", sa.String(length=64), nullable=True),
+        sa.Column("error_message", sa.Text(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_team_run_team_id", "team_run", ["team_id"], unique=False)
+    op.create_index("ix_team_run_team_version_id", "team_run", ["team_version_id"], unique=False)
+    op.create_index("ix_team_run_session_id", "team_run", ["session_id"], unique=False)
+    op.create_index("ix_team_run_status", "team_run", ["status"], unique=False)
+    op.create_index("ix_team_run_tenant_id", "team_run", ["tenant_id"], unique=False)
+
+
+def downgrade() -> None:
+    op.drop_index("ix_team_run_tenant_id", table_name="team_run")
+    op.drop_index("ix_team_run_status", table_name="team_run")
+    op.drop_index("ix_team_run_session_id", table_name="team_run")
+    op.drop_index("ix_team_run_team_version_id", table_name="team_run")
+    op.drop_index("ix_team_run_team_id", table_name="team_run")
+    op.drop_table("team_run")
+
+    op.drop_index("ix_team_version_tenant_id", table_name="team_version")
+    op.drop_index("ix_team_version_status", table_name="team_version")
+    op.drop_index("ix_team_version_team_id", table_name="team_version")
+    op.drop_table("team_version")
+
+    op.drop_index("ix_team_definition_tenant_id", table_name="team_definition")
+    op.drop_index("ix_team_definition_status", table_name="team_definition")
+    op.drop_index("ix_team_definition_code", table_name="team_definition")
+    op.drop_table("team_definition")

+ 1 - 0
services/team-service/app/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
services/team-service/app/api/__init__.py

@@ -0,0 +1 @@
+

+ 133 - 0
services/team-service/app/api/routes.py

@@ -0,0 +1,133 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceHealth
+
+from app.application.services import TeamApplicationService
+from app.db.session import get_db
+from app.domain.repositories import (
+    TeamDefinitionRepository,
+    TeamRunRepository,
+    TeamVersionRepository,
+)
+from app.schemas.team import (
+    TeamCreateRequest,
+    TeamResponse,
+    TeamRunCreateRequest,
+    TeamRunResponse,
+    TeamRunStatusUpdateRequest,
+    TeamStatusUpdateRequest,
+    TeamVersionCreateRequest,
+    TeamVersionResponse,
+)
+
+router = APIRouter()
+
+
+def get_team_application_service(db: Session = Depends(get_db)) -> TeamApplicationService:
+    return TeamApplicationService(
+        team_repository=TeamDefinitionRepository(db),
+        team_version_repository=TeamVersionRepository(db),
+        team_run_repository=TeamRunRepository(db),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="team-service", status="ok", database="ok")
+
+
+@router.post("", response_model=TeamResponse)
+def create_team(
+    payload: TeamCreateRequest,
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> TeamResponse:
+    entity = service.create_team(payload)
+    return TeamResponse.from_entity(entity)
+
+
+@router.get("", response_model=list[TeamResponse])
+def list_teams(
+    tenant_id: str = Query(...),
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> list[TeamResponse]:
+    return [TeamResponse.from_entity(item) for item in service.list_teams(tenant_id=tenant_id)]
+
+
+@router.patch("/{team_id}/status", response_model=TeamResponse)
+def update_team_status(
+    team_id: str,
+    payload: TeamStatusUpdateRequest,
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> TeamResponse:
+    entity = service.update_team_status(team_id=team_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"team not found: {team_id}")
+    return TeamResponse.from_entity(entity)
+
+
+@router.post("/versions", response_model=TeamVersionResponse)
+def create_team_version(
+    payload: TeamVersionCreateRequest,
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> TeamVersionResponse:
+    try:
+        entity = service.create_team_version(payload)
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    return TeamVersionResponse.from_entity(entity)
+
+
+@router.get("/versions", response_model=list[TeamVersionResponse])
+def list_team_versions(
+    tenant_id: str = Query(...),
+    team_id: str = Query(...),
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> list[TeamVersionResponse]:
+    return [
+        TeamVersionResponse.from_entity(item)
+        for item in service.list_team_versions(tenant_id=tenant_id, team_id=team_id)
+    ]
+
+
+@router.post("/runs", response_model=TeamRunResponse)
+def create_team_run(
+    payload: TeamRunCreateRequest,
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> TeamRunResponse:
+    try:
+        entity = service.create_team_run(payload)
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    return TeamRunResponse.from_entity(entity)
+
+
+@router.get("/runs", response_model=list[TeamRunResponse])
+def list_team_runs(
+    tenant_id: str = Query(...),
+    team_id: str | None = Query(default=None),
+    session_id: str | None = Query(default=None),
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> list[TeamRunResponse]:
+    return [
+        TeamRunResponse.from_entity(item)
+        for item in service.list_team_runs(
+            tenant_id=tenant_id,
+            team_id=team_id,
+            session_id=session_id,
+        )
+    ]
+
+
+@router.post("/runs/{team_run_id}/status", response_model=TeamRunResponse)
+def update_team_run_status(
+    team_run_id: str,
+    payload: TeamRunStatusUpdateRequest,
+    service: TeamApplicationService = Depends(get_team_application_service),
+) -> TeamRunResponse:
+    entity = service.update_team_run_status(team_run_id=team_run_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"team_run not found: {team_run_id}")
+    return TeamRunResponse.from_entity(entity)

+ 1 - 0
services/team-service/app/application/__init__.py

@@ -0,0 +1 @@
+

+ 142 - 0
services/team-service/app/application/services.py

@@ -0,0 +1,142 @@
+from app.db.models import TeamDefinition, TeamRun, TeamVersion
+from app.domain.repositories import (
+    TeamDefinitionRepository,
+    TeamRunRepository,
+    TeamVersionRepository,
+)
+from app.schemas.team import (
+    TeamCreateRequest,
+    TeamRunCreateRequest,
+    TeamRunStatusUpdateRequest,
+    TeamStatusUpdateRequest,
+    TeamVersionCreateRequest,
+)
+
+
+class TeamApplicationService:
+    def __init__(
+        self,
+        *,
+        team_repository: TeamDefinitionRepository,
+        team_version_repository: TeamVersionRepository,
+        team_run_repository: TeamRunRepository,
+    ) -> None:
+        self.team_repository = team_repository
+        self.team_version_repository = team_version_repository
+        self.team_run_repository = team_run_repository
+
+    def create_team(self, payload: TeamCreateRequest) -> TeamDefinition:
+        return self.team_repository.create(
+            tenant_id=payload.tenant_id,
+            code=payload.code,
+            name=payload.name,
+            description=payload.description,
+            team_type=payload.team_type,
+            owner_user_id=payload.owner_user_id,
+            metadata_json=payload.metadata_json,
+        )
+
+    def list_teams(self, *, tenant_id: str) -> list[TeamDefinition]:
+        return self.team_repository.list_by_tenant(tenant_id=tenant_id)
+
+    def update_team_status(
+        self,
+        *,
+        team_id: str,
+        payload: TeamStatusUpdateRequest,
+    ) -> TeamDefinition | None:
+        return self.team_repository.update_status(
+            tenant_id=payload.tenant_id,
+            team_id=team_id,
+            status=payload.status,
+        )
+
+    def create_team_version(self, payload: TeamVersionCreateRequest) -> TeamVersion:
+        team = self.team_repository.get_by_id(tenant_id=payload.tenant_id, team_id=payload.team_id)
+        if team is None:
+            raise ValueError(f"team not found: {payload.team_id}")
+        if not payload.member_refs:
+            raise ValueError("team version requires at least one member")
+
+        return self.team_version_repository.create(
+            tenant_id=payload.tenant_id,
+            team_id=payload.team_id,
+            status=payload.status,
+            coordination_mode=payload.coordination_mode,
+            objective=payload.objective,
+            member_refs_json=[item.model_dump(mode="json") for item in payload.member_refs],
+            policy_json=payload.policy_json,
+        )
+
+    def list_team_versions(self, *, tenant_id: str, team_id: str) -> list[TeamVersion]:
+        return self.team_version_repository.list_by_team(tenant_id=tenant_id, team_id=team_id)
+
+    def create_team_run(self, payload: TeamRunCreateRequest) -> TeamRun:
+        team_version = self._resolve_team_version(
+            tenant_id=payload.tenant_id,
+            team_id=payload.team_id,
+            team_version_id=payload.team_version_id,
+        )
+        if team_version is None:
+            raise ValueError("published team version not found")
+
+        return self.team_run_repository.create(
+            tenant_id=payload.tenant_id,
+            team_id=payload.team_id,
+            team_version_id=team_version.id,
+            session_id=payload.session_id,
+            input_text=payload.input_text,
+            input_json=payload.input_json,
+        )
+
+    def list_team_runs(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str | None = None,
+        session_id: str | None = None,
+    ) -> list[TeamRun]:
+        return self.team_run_repository.list_by_scope(
+            tenant_id=tenant_id,
+            team_id=team_id,
+            session_id=session_id,
+        )
+
+    def update_team_run_status(
+        self,
+        *,
+        team_run_id: str,
+        payload: TeamRunStatusUpdateRequest,
+    ) -> TeamRun | None:
+        entity = self.team_run_repository.get_by_id(
+            tenant_id=payload.tenant_id,
+            team_run_id=team_run_id,
+        )
+        if entity is None:
+            return None
+        return self.team_run_repository.update_status(
+            team_run_id=team_run_id,
+            status=payload.status,
+            worker_key=payload.worker_key,
+            output_text=payload.output_text,
+            output_json=payload.output_json,
+            error_code=payload.error_code,
+            error_message=payload.error_message,
+        )
+
+    def _resolve_team_version(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str,
+        team_version_id: str | None,
+    ) -> TeamVersion | None:
+        if team_version_id is not None:
+            return self.team_version_repository.get_by_id(
+                tenant_id=tenant_id,
+                team_version_id=team_version_id,
+            )
+        return self.team_version_repository.get_latest_published(
+            tenant_id=tenant_id,
+            team_id=team_id,
+        )

+ 1 - 0
services/team-service/app/bootstrap/__init__.py

@@ -0,0 +1 @@
+

+ 17 - 0
services/team-service/app/bootstrap/app.py

@@ -0,0 +1,17 @@
+from fastapi import FastAPI
+
+from app.api.routes import router
+from app.bootstrap.settings import TeamServiceSettings
+from app.db.session import build_session_factory
+
+
+def create_app() -> FastAPI:
+    settings = TeamServiceSettings()
+    app = FastAPI(
+        title="agent-platform team-service",
+        version="0.1.0",
+    )
+    app.state.settings = settings
+    app.state.session_factory = build_session_factory(settings)
+    app.include_router(router, prefix="/teams", tags=["teams"])
+    return app

+ 7 - 0
services/team-service/app/bootstrap/settings.py

@@ -0,0 +1,7 @@
+from core_shared import ServiceSettings
+
+
+class TeamServiceSettings(ServiceSettings):
+    service_name: str = "team-service"
+    service_port: int = 8009
+    database_url: str = "sqlite:///./team_service.db"

+ 1 - 0
services/team-service/app/db/__init__.py

@@ -0,0 +1 @@
+

+ 7 - 0
services/team-service/app/db/models/__init__.py

@@ -0,0 +1,7 @@
+from core_db import Base
+
+from .team_definition import TeamDefinition
+from .team_run import TeamRun
+from .team_version import TeamVersion
+
+__all__ = ["Base", "TeamDefinition", "TeamRun", "TeamVersion"]

+ 18 - 0
services/team-service/app/db/models/team_definition.py

@@ -0,0 +1,18 @@
+from sqlalchemy import String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class TeamDefinition(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "team_definition"
+
+    code: Mapped[str] = mapped_column(String(64), index=True)
+    name: Mapped[str] = mapped_column(String(128))
+    description: Mapped[str | None] = mapped_column(Text, nullable=True)
+    team_type: Mapped[str] = mapped_column(String(32), default="collaborative")
+    status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
+    owner_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    metadata_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)

+ 28 - 0
services/team-service/app/db/models/team_run.py

@@ -0,0 +1,28 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class TeamRun(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "team_run"
+
+    team_id: Mapped[str] = mapped_column(String(36), index=True)
+    team_version_id: Mapped[str] = mapped_column(String(36), index=True)
+    session_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
+    status: Mapped[str] = mapped_column(String(32), default="queued", index=True)
+    worker_key: Mapped[str | None] = mapped_column(String(128), nullable=True)
+    queued_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    lease_expire_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    input_text: Mapped[str | None] = mapped_column(Text, nullable=True)
+    input_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)
+    output_text: Mapped[str | None] = mapped_column(Text, nullable=True)
+    output_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)
+    started_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    finished_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
+    error_message: Mapped[str | None] = mapped_column(Text, nullable=True)

+ 21 - 0
services/team-service/app/db/models/team_version.py

@@ -0,0 +1,21 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, Integer, String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class TeamVersion(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "team_version"
+
+    team_id: Mapped[str] = mapped_column(String(36), index=True)
+    version_no: Mapped[int] = mapped_column(Integer)
+    status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
+    coordination_mode: Mapped[str] = mapped_column(String(64), default="supervisor")
+    objective: Mapped[str | None] = mapped_column(Text, nullable=True)
+    member_refs_json: Mapped[list[dict[str, JSONValue]]] = mapped_column(JSON, default=list)
+    policy_json: Mapped[dict[str, JSONValue]] = mapped_column(JSON, default=dict)
+    published_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

+ 27 - 0
services/team-service/app/db/session.py

@@ -0,0 +1,27 @@
+from collections.abc import Generator
+
+from fastapi import Request
+from sqlalchemy.orm import Session, sessionmaker
+
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
+
+from app.bootstrap.settings import TeamServiceSettings
+
+
+def build_session_factory(settings: TeamServiceSettings | None = None) -> sessionmaker[Session]:
+    resolved_settings = settings or TeamServiceSettings()
+    db_settings = DatabaseSettings(
+        database_url=resolved_settings.database_url,
+        echo_sql=resolved_settings.echo_sql,
+    )
+    engine = create_engine_from_settings(db_settings)
+    return create_session_factory(engine)
+
+
+def get_db(request: Request) -> Generator[Session, None, None]:
+    session_factory: sessionmaker[Session] = request.app.state.session_factory
+    session = session_factory()
+    try:
+        yield session
+    finally:
+        session.close()

+ 1 - 0
services/team-service/app/domain/__init__.py

@@ -0,0 +1 @@
+

+ 222 - 0
services/team-service/app/domain/repositories.py

@@ -0,0 +1,222 @@
+from datetime import datetime
+
+from sqlalchemy import func, select
+from sqlalchemy.orm import Session
+
+from core_domain import TeamRunStatus, TeamStatus, TeamVersionStatus
+from core_shared import JSONValue
+
+from app.db.models import TeamDefinition, TeamRun, TeamVersion
+
+
+class TeamDefinitionRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        code: str,
+        name: str,
+        description: str | None,
+        team_type: str,
+        owner_user_id: str | None,
+        metadata_json: dict[str, JSONValue] | None,
+    ) -> TeamDefinition:
+        entity = TeamDefinition(
+            tenant_id=tenant_id,
+            code=code,
+            name=name,
+            description=description,
+            team_type=team_type,
+            owner_user_id=owner_user_id,
+            metadata_json=metadata_json,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_tenant(self, *, tenant_id: str) -> list[TeamDefinition]:
+        stmt = (
+            select(TeamDefinition)
+            .where(TeamDefinition.tenant_id == tenant_id)
+            .order_by(TeamDefinition.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, team_id: str) -> TeamDefinition | None:
+        stmt = (
+            select(TeamDefinition)
+            .where(TeamDefinition.tenant_id == tenant_id)
+            .where(TeamDefinition.id == team_id)
+        )
+        return self.db.scalar(stmt)
+
+    def update_status(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str,
+        status: TeamStatus,
+    ) -> TeamDefinition | None:
+        entity = self.get_by_id(tenant_id=tenant_id, team_id=team_id)
+        if entity is None:
+            return None
+        entity.status = status
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+
+class TeamVersionRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str,
+        status: TeamVersionStatus,
+        coordination_mode: str,
+        objective: str | None,
+        member_refs_json: list[dict[str, JSONValue]],
+        policy_json: dict[str, JSONValue],
+    ) -> TeamVersion:
+        version_no = self._next_version_no(team_id)
+        entity = TeamVersion(
+            tenant_id=tenant_id,
+            team_id=team_id,
+            version_no=version_no,
+            status=status,
+            coordination_mode=coordination_mode,
+            objective=objective,
+            member_refs_json=member_refs_json,
+            policy_json=policy_json,
+            published_time=datetime.utcnow() if status == "published" else None,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_team(self, *, tenant_id: str, team_id: str) -> list[TeamVersion]:
+        stmt = (
+            select(TeamVersion)
+            .where(TeamVersion.tenant_id == tenant_id)
+            .where(TeamVersion.team_id == team_id)
+            .order_by(TeamVersion.version_no.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, team_version_id: str) -> TeamVersion | None:
+        stmt = (
+            select(TeamVersion)
+            .where(TeamVersion.tenant_id == tenant_id)
+            .where(TeamVersion.id == team_version_id)
+        )
+        return self.db.scalar(stmt)
+
+    def get_latest_published(self, *, tenant_id: str, team_id: str) -> TeamVersion | None:
+        stmt = (
+            select(TeamVersion)
+            .where(TeamVersion.tenant_id == tenant_id)
+            .where(TeamVersion.team_id == team_id)
+            .where(TeamVersion.status == "published")
+            .order_by(TeamVersion.version_no.desc())
+            .limit(1)
+        )
+        return self.db.scalar(stmt)
+
+    def _next_version_no(self, team_id: str) -> int:
+        stmt = select(func.max(TeamVersion.version_no)).where(TeamVersion.team_id == team_id)
+        current_max = self.db.scalar(stmt)
+        return (current_max or 0) + 1
+
+
+class TeamRunRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str,
+        team_version_id: str,
+        session_id: str | None,
+        input_text: str | None,
+        input_json: dict[str, JSONValue] | None,
+    ) -> TeamRun:
+        now = datetime.utcnow()
+        entity = TeamRun(
+            tenant_id=tenant_id,
+            team_id=team_id,
+            team_version_id=team_version_id,
+            session_id=session_id,
+            input_text=input_text,
+            input_json=input_json,
+            status="queued",
+            queued_time=now,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_scope(
+        self,
+        *,
+        tenant_id: str,
+        team_id: str | None = None,
+        session_id: str | None = None,
+    ) -> list[TeamRun]:
+        stmt = select(TeamRun).where(TeamRun.tenant_id == tenant_id)
+        if team_id is not None:
+            stmt = stmt.where(TeamRun.team_id == team_id)
+        if session_id is not None:
+            stmt = stmt.where(TeamRun.session_id == session_id)
+        stmt = stmt.order_by(TeamRun.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, team_run_id: str) -> TeamRun | None:
+        stmt = (
+            select(TeamRun)
+            .where(TeamRun.tenant_id == tenant_id)
+            .where(TeamRun.id == team_run_id)
+        )
+        return self.db.scalar(stmt)
+
+    def update_status(
+        self,
+        *,
+        team_run_id: str,
+        status: TeamRunStatus,
+        worker_key: str | None = None,
+        output_text: str | None = None,
+        output_json: dict[str, JSONValue] | None = None,
+        error_code: str | None = None,
+        error_message: str | None = None,
+    ) -> TeamRun | None:
+        entity = self.db.get(TeamRun, team_run_id)
+        if entity is None:
+            return None
+
+        now = datetime.utcnow()
+        entity.status = status
+        entity.worker_key = worker_key
+        entity.output_text = output_text
+        entity.output_json = output_json
+        entity.error_code = error_code
+        entity.error_message = error_message
+        if status == "running" and entity.started_time is None:
+            entity.started_time = now
+        if status in {"completed", "failed", "cancelled"}:
+            entity.finished_time = now
+            entity.lease_expire_time = None
+
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity

+ 3 - 0
services/team-service/app/main.py

@@ -0,0 +1,3 @@
+from app.bootstrap.app import create_app
+
+app = create_app()

+ 1 - 0
services/team-service/app/schemas/__init__.py

@@ -0,0 +1 @@
+

+ 79 - 0
services/team-service/app/schemas/team.py

@@ -0,0 +1,79 @@
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel, Field
+
+from core_domain import (
+    TeamDefinitionContract,
+    TeamMemberContract,
+    TeamRunContract,
+    TeamRunStatus,
+    TeamStatus,
+    TeamVersionContract,
+    TeamVersionStatus,
+)
+from core_shared import JSONValue
+
+if TYPE_CHECKING:
+    from app.db.models import TeamDefinition, TeamRun, TeamVersion
+
+
+class TeamCreateRequest(BaseModel):
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    team_type: str = "collaborative"
+    owner_user_id: str | None = None
+    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class TeamStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: TeamStatus
+
+
+class TeamResponse(TeamDefinitionContract):
+    @classmethod
+    def from_entity(cls, entity: "TeamDefinition") -> "TeamResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class TeamVersionCreateRequest(BaseModel):
+    tenant_id: str
+    team_id: str
+    status: TeamVersionStatus = "draft"
+    coordination_mode: str = "supervisor"
+    objective: str | None = None
+    member_refs: list[TeamMemberContract] = Field(default_factory=list)
+    policy_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class TeamVersionResponse(TeamVersionContract):
+    @classmethod
+    def from_entity(cls, entity: "TeamVersion") -> "TeamVersionResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class TeamRunCreateRequest(BaseModel):
+    tenant_id: str
+    team_id: str
+    team_version_id: str | None = None
+    session_id: str | None = None
+    input_text: str | None = None
+    input_json: dict[str, JSONValue] | None = None
+
+
+class TeamRunStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: TeamRunStatus
+    worker_key: str | None = None
+    output_text: str | None = None
+    output_json: dict[str, JSONValue] | None = None
+    error_code: str | None = None
+    error_message: str | None = None
+
+
+class TeamRunResponse(TeamRunContract):
+    @classmethod
+    def from_entity(cls, entity: "TeamRun") -> "TeamRunResponse":
+        return cls.model_validate(entity, from_attributes=True)

+ 25 - 0
services/team-service/pyproject.toml

@@ -0,0 +1,25 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "team-service"
+version = "0.1.0"
+description = "Team orchestration service for agent platform."
+requires-python = ">=3.11"
+dependencies = [
+  "alembic>=1.13,<2.0",
+  "fastapi>=0.111,<1.0",
+  "uvicorn[standard]>=0.30,<1.0",
+  "pydantic>=2.7,<3.0",
+  "sqlalchemy>=2.0,<3.0",
+  "core-db",
+  "core-domain",
+  "core-shared",
+]
+
+[tool.setuptools]
+package-dir = {"" = "."}
+
+[tool.setuptools.packages.find]
+where = ["."]