diff --git a/.gitignore b/.gitignore index 28597f6..9d75fce 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,6 @@ robolearn-interface/ .env .env.local -cookies.txt \ No newline at end of file +cookies.txt + +rag-agent/ \ No newline at end of file diff --git a/packages/api/pyproject.toml b/packages/api/pyproject.toml index 36e0285..7cbeed1 100644 --- a/packages/api/pyproject.toml +++ b/packages/api/pyproject.toml @@ -14,6 +14,10 @@ dependencies = [ "uvicorn[standard]>=0.32.0", "asyncpg>=0.30.0", "sqlalchemy[asyncio]>=2.0.0", + "python-dotenv>=1.0.0", + # ChatKit + OpenAI Agents SDK for chat server + "openai-agents>=0.0.9", + "openai-chatkit>=1.4.0", ] [project.optional-dependencies] diff --git a/packages/api/src/taskflow_api/chatkit_store/__init__.py b/packages/api/src/taskflow_api/chatkit_store/__init__.py new file mode 100644 index 0000000..a4c3ad9 --- /dev/null +++ b/packages/api/src/taskflow_api/chatkit_store/__init__.py @@ -0,0 +1,16 @@ +""" +TaskFlow ChatKit Store Implementation. + +Provides PostgreSQL-based Store for ChatKit conversation persistence. +Adapted from rag-agent/chatkit_store for TaskFlow API. +""" + +from .config import StoreConfig +from .context import RequestContext +from .postgres_store import PostgresStore + +__all__ = [ + "StoreConfig", + "PostgresStore", + "RequestContext", +] diff --git a/packages/api/src/taskflow_api/chatkit_store/config.py b/packages/api/src/taskflow_api/chatkit_store/config.py new file mode 100644 index 0000000..c2ab4af --- /dev/null +++ b/packages/api/src/taskflow_api/chatkit_store/config.py @@ -0,0 +1,83 @@ +"""Configuration for TaskFlow ChatKit store.""" + +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class StoreConfig(BaseSettings): + """ + PostgreSQL Store configuration for TaskFlow ChatKit. + + All settings can be overridden via environment variables with the + TASKFLOW_CHATKIT_ prefix (e.g., TASKFLOW_CHATKIT_DATABASE_URL). + """ + + model_config = SettingsConfigDict( + env_prefix="TASKFLOW_CHATKIT_", + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + # Database connection + database_url: str = Field( + ..., + description="PostgreSQL connection URL (postgresql+asyncpg://user:pass@host:port/db)", + ) + + # Connection pool settings + pool_size: int = Field( + default=20, + description="Maximum number of connections in the pool", + ge=1, + le=100, + ) + + max_overflow: int = Field( + default=10, + description="Maximum overflow connections beyond pool_size", + ge=0, + le=50, + ) + + pool_timeout: float = Field( + default=30.0, + description="Seconds to wait before timing out on getting a connection", + gt=0, + ) + + pool_recycle: int = Field( + default=3600, description="Seconds after which connections are recycled", ge=300 + ) + + # Query settings + statement_timeout: int = Field( + default=30000, description="Statement timeout in milliseconds", ge=1000 + ) + + # Schema - use taskflow_chat to isolate from main taskflow schema + schema_name: str = Field(default="taskflow_chat", description="Database schema name for tables") + + @field_validator("database_url") + @classmethod + def validate_database_url(cls, v: str) -> str: + """Ensure database URL uses asyncpg driver and fix SSL parameters.""" + if not v.startswith("postgresql://") and not v.startswith("postgresql+asyncpg://"): + raise ValueError("database_url must start with postgresql:// or postgresql+asyncpg://") + + # Convert to asyncpg if needed + if v.startswith("postgresql://"): + v = v.replace("postgresql://", "postgresql+asyncpg://", 1) + + # Fix SSL parameters for asyncpg + if "sslmode=require" in v: + v = v.replace("sslmode=require", "ssl=require") + elif "sslmode=prefer" in v: + v = v.replace("sslmode=prefer", "ssl=prefer") + elif "sslmode=allow" in v: + v = v.replace("sslmode=allow", "ssl=allow") + elif "sslmode=disable" in v: + v = v.replace("sslmode=disable", "ssl=disable") + + return v diff --git a/packages/api/src/taskflow_api/chatkit_store/context.py b/packages/api/src/taskflow_api/chatkit_store/context.py new file mode 100644 index 0000000..1a45ac3 --- /dev/null +++ b/packages/api/src/taskflow_api/chatkit_store/context.py @@ -0,0 +1,37 @@ +"""Request context for ChatKit store operations.""" + +from typing import Any + +from pydantic import BaseModel, Field + + +class RequestContext(BaseModel): + """ + Request context passed to all store operations. + + Provides user isolation and optional metadata for access control, + logging, and tracing across all store operations. + """ + + user_id: str | None = Field( + ..., + description="Unique identifier for the user making the request", + min_length=1, + ) + + organization_id: str | None = Field( + default=None, description="Optional organization ID for multi-org tenancy" + ) + + request_id: str | None = Field( + default=None, description="Optional request ID for tracing and logging" + ) + + metadata: dict[str, Any] = Field( + default_factory=dict, description="Additional context metadata" + ) + + class Config: + """Pydantic configuration.""" + + frozen = False # Allow mutation for adding trace info diff --git a/packages/api/src/taskflow_api/chatkit_store/postgres_store.py b/packages/api/src/taskflow_api/chatkit_store/postgres_store.py new file mode 100644 index 0000000..2adadf8 --- /dev/null +++ b/packages/api/src/taskflow_api/chatkit_store/postgres_store.py @@ -0,0 +1,579 @@ +"""PostgreSQL-based Store implementation for ChatKit.""" + +import asyncio +import logging +from datetime import datetime +from typing import Any + +from chatkit.store import NotFoundError, Store +from chatkit.types import Attachment, Page, ThreadItem, ThreadMetadata +from pydantic import BaseModel +from sqlalchemy import text +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) + +from .config import StoreConfig +from .context import RequestContext + +logger = logging.getLogger(__name__) + + +class ThreadData(BaseModel): + """Wrapper for thread serialization.""" + + thread: ThreadMetadata + + +class ItemData(BaseModel): + """Wrapper for item serialization.""" + + item: ThreadItem + + +class AttachmentData(BaseModel): + """Wrapper for attachment serialization.""" + + attachment: Attachment + + +class PostgresStore(Store[RequestContext]): + """ + Production-ready PostgreSQL store for ChatKit. + + Features: + - Async connection pooling + - User-based multi-tenancy + - JSON serialization for flexible schema evolution + - Proper indexing for performance + - Statement timeouts + - Error handling and logging + """ + + def __init__( + self, + config: StoreConfig | None = None, + engine: AsyncEngine | None = None, + ): + """ + Initialize the PostgreSQL store. + + Args: + config: Store configuration. If None, loads from environment. + engine: Optional pre-configured SQLAlchemy engine. + If provided, config is ignored. + """ + if engine: + self.engine = engine + self.config = None + elif config: + self.config = config + self.engine = self._create_engine(config) + else: + self.config = StoreConfig() + self.engine = self._create_engine(self.config) + + self.session_factory = async_sessionmaker( + self.engine, + class_=AsyncSession, + expire_on_commit=False, + ) + + logger.info("PostgresStore initialized for TaskFlow ChatKit") + + def _create_engine(self, config: StoreConfig) -> AsyncEngine: + """Create SQLAlchemy async engine with connection pooling.""" + return create_async_engine( + config.database_url, + pool_size=config.pool_size, + max_overflow=config.max_overflow, + pool_timeout=config.pool_timeout, + pool_recycle=config.pool_recycle, + pool_pre_ping=True, + echo=False, + future=True, + connect_args={ + "command_timeout": 30, + "server_settings": { + "application_name": "taskflow_chatkit", + "jit": "off", + "statement_timeout": str(config.statement_timeout), + }, + }, + ) + + async def initialize_schema(self) -> None: + """ + Create database schema and tables if they don't exist. + + Should be called during application startup. + """ + schema = self.config.schema_name if self.config else "taskflow_chat" + + async with self.engine.connect() as conn: + result = await conn.execute( + text(""" + SELECT EXISTS ( + SELECT FROM information_schema.schemata + WHERE schema_name = :schema_name + ) + """), + {"schema_name": schema}, + ) + + schema_exists = result.scalar() + if schema_exists: + logger.info(f"Schema '{schema}' already exists, skipping initialization") + return + + logger.info(f"Initializing schema '{schema}'...") + + async with self.engine.begin() as conn: + await conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {schema}")) + + await conn.execute( + text(f""" + CREATE TABLE IF NOT EXISTS {schema}.threads ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + organization_id TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + data JSONB NOT NULL, + CONSTRAINT threads_id_user_unique UNIQUE (id, user_id) + ) + """) + ) + + await conn.execute( + text(f""" + CREATE TABLE IF NOT EXISTS {schema}.items ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + user_id TEXT NOT NULL, + organization_id TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + data JSONB NOT NULL, + CONSTRAINT items_id_user_unique UNIQUE (id, user_id) + ) + """) + ) + + await conn.execute( + text(f""" + CREATE TABLE IF NOT EXISTS {schema}.attachments ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + organization_id TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + data JSONB NOT NULL, + CONSTRAINT attachments_id_user_unique UNIQUE (id, user_id) + ) + """) + ) + + # Create indexes + await conn.execute( + text(f""" + CREATE INDEX IF NOT EXISTS idx_threads_user_created + ON {schema}.threads (user_id, created_at DESC) + """) + ) + + await conn.execute( + text(f""" + CREATE INDEX IF NOT EXISTS idx_threads_org_created + ON {schema}.threads (organization_id, created_at DESC) + WHERE organization_id IS NOT NULL + """) + ) + + await conn.execute( + text(f""" + CREATE INDEX IF NOT EXISTS idx_items_thread_created + ON {schema}.items (thread_id, created_at) + """) + ) + + await conn.execute( + text(f""" + CREATE INDEX IF NOT EXISTS idx_items_user_thread + ON {schema}.items (user_id, thread_id) + """) + ) + + await conn.execute( + text(f""" + CREATE INDEX IF NOT EXISTS idx_attachments_user + ON {schema}.attachments (user_id, created_at DESC) + """) + ) + + await conn.commit() + + logger.info(f"Database schema '{schema}' initialized successfully") + + await self._warm_connection_pool() + + async def _warm_connection_pool(self) -> None: + """Warm up the connection pool to reduce cold start latency.""" + try: + warmup_tasks = [] + for _ in range(min(3, self.config.pool_size if self.config else 5)): + warmup_tasks.append(self._warm_single_connection()) + + await asyncio.gather(*warmup_tasks, return_exceptions=True) + logger.info("Connection pool warmed up") + except Exception as e: + logger.warning(f"Failed to warm connection pool: {e}") + + async def _warm_single_connection(self) -> None: + """Warm up a single connection.""" + async with self.engine.connect() as conn: + await conn.execute(text("SELECT 1")) + + def _get_table_name(self, table: str) -> str: + """Get fully qualified table name with schema.""" + schema = self.config.schema_name if self.config else "taskflow_chat" + return f"{schema}.{table}" + + async def load_thread(self, thread_id: str, context: RequestContext) -> ThreadMetadata: + """Load a thread by ID with user isolation.""" + async with self.session_factory() as session: + result = await session.execute( + text(f""" + SELECT data FROM {self._get_table_name("threads")} + WHERE id = :thread_id AND user_id = :user_id + """), + {"thread_id": thread_id, "user_id": context.user_id}, + ) + row = result.first() + + if not row: + raise NotFoundError(f"Thread {thread_id} not found") + + return ThreadData.model_validate(row[0]).thread + + async def save_thread(self, thread: ThreadMetadata, context: RequestContext) -> None: + """Save or update a thread.""" + thread_data = ThreadData(thread=thread) + + async with self.session_factory() as session: + try: + await session.execute( + text(f""" + INSERT INTO {self._get_table_name("threads")} + (id, user_id, organization_id, created_at, updated_at, data) + VALUES (:id, :user_id, :org_id, :created_at, NOW(), :data) + ON CONFLICT (id, user_id) DO UPDATE SET + data = EXCLUDED.data, + updated_at = NOW() + """), + { + "id": thread.id, + "user_id": context.user_id, + "org_id": context.organization_id, + "created_at": thread.created_at, + "data": thread_data.model_dump_json(), + }, + ) + await session.commit() + except Exception as e: + await session.rollback() + logger.error(f"Failed to save thread {thread.id}: {e}") + raise + + async def load_thread_items( + self, + thread_id: str, + after: str | None, + limit: int, + order: str, + context: RequestContext, + ) -> Page[ThreadItem]: + """Load paginated thread items.""" + async with self.session_factory() as session: + created_after: datetime | None = None + + if after: + result = await session.execute( + text(f""" + SELECT created_at FROM {self._get_table_name("items")} + WHERE id = :after_id AND user_id = :user_id + """), + {"after_id": after, "user_id": context.user_id}, + ) + row = result.first() + if not row: + raise NotFoundError(f"Item {after} not found") + created_after = row[0] + + params: dict[str, Any] = { + "thread_id": thread_id, + "user_id": context.user_id, + "limit": limit + 1, + } + + if created_after: + comparison = ">" if order == "asc" else "<" + params["created_after"] = created_after + created_clause = f"AND created_at {comparison} :created_after" + else: + created_clause = "" + + result = await session.execute( + text(f""" + SELECT id, data FROM {self._get_table_name("items")} + WHERE thread_id = :thread_id AND user_id = :user_id + {created_clause} + ORDER BY created_at {order} + LIMIT :limit + """), + params, + ) + + items = [ItemData.model_validate(row[1]).item for row in result.fetchall()] + + has_more = len(items) > limit + if has_more: + items = items[:limit] + + next_after = items[-1].id if (has_more and items) else None + + return Page[ThreadItem]( + data=items, + has_more=has_more, + after=next_after, + ) + + async def add_thread_item( + self, thread_id: str, item: ThreadItem, context: RequestContext + ) -> None: + """Add a new item to a thread.""" + item_data = ItemData(item=item) + + async with self.session_factory() as session: + await session.execute( + text(f""" + INSERT INTO {self._get_table_name("items")} + (id, thread_id, user_id, organization_id, created_at, data) + VALUES (:id, :thread_id, :user_id, :org_id, :created_at, :data) + """), + { + "id": item.id, + "thread_id": thread_id, + "user_id": context.user_id, + "org_id": context.organization_id, + "created_at": item.created_at, + "data": item_data.model_dump_json(), + }, + ) + await session.commit() + + async def save_item(self, thread_id: str, item: ThreadItem, context: RequestContext) -> None: + """Update an existing item.""" + item_data = ItemData(item=item) + + async with self.session_factory() as session: + result = await session.execute( + text(f""" + UPDATE {self._get_table_name("items")} + SET data = :data, updated_at = NOW() + WHERE id = :id AND thread_id = :thread_id AND user_id = :user_id + """), + { + "id": item.id, + "thread_id": thread_id, + "user_id": context.user_id, + "data": item_data.model_dump_json(), + }, + ) + await session.commit() + + if result.rowcount == 0: + raise NotFoundError(f"Item {item.id} not found in thread {thread_id}") + + async def load_item(self, thread_id: str, item_id: str, context: RequestContext) -> ThreadItem: + """Load a specific item by ID.""" + async with self.session_factory() as session: + result = await session.execute( + text(f""" + SELECT data FROM {self._get_table_name("items")} + WHERE id = :item_id + AND thread_id = :thread_id + AND user_id = :user_id + """), + { + "item_id": item_id, + "thread_id": thread_id, + "user_id": context.user_id, + }, + ) + row = result.first() + + if not row: + raise NotFoundError(f"Item {item_id} not found in thread {thread_id}") + + return ItemData.model_validate(row[0]).item + + async def delete_thread_item( + self, thread_id: str, item_id: str, context: RequestContext + ) -> None: + """Delete an item from a thread.""" + async with self.session_factory() as session: + await session.execute( + text(f""" + DELETE FROM {self._get_table_name("items")} + WHERE id = :item_id + AND thread_id = :thread_id + AND user_id = :user_id + """), + { + "item_id": item_id, + "thread_id": thread_id, + "user_id": context.user_id, + }, + ) + await session.commit() + + async def load_threads( + self, + limit: int, + after: str | None, + order: str, + context: RequestContext, + ) -> Page[ThreadMetadata]: + """Load paginated list of threads for a user.""" + async with self.session_factory() as session: + created_after: datetime | None = None + + if after: + result = await session.execute( + text(f""" + SELECT created_at FROM {self._get_table_name("threads")} + WHERE id = :after_id AND user_id = :user_id + """), + {"after_id": after, "user_id": context.user_id}, + ) + row = result.first() + if not row: + raise NotFoundError(f"Thread {after} not found") + created_after = row[0] + + params: dict[str, Any] = { + "user_id": context.user_id, + "limit": limit + 1, + } + + if created_after: + comparison = ">" if order == "asc" else "<" + params["created_after"] = created_after + created_clause = f"AND created_at {comparison} :created_after" + else: + created_clause = "" + + result = await session.execute( + text(f""" + SELECT data FROM {self._get_table_name("threads")} + WHERE user_id = :user_id + {created_clause} + ORDER BY created_at {order} + LIMIT :limit + """), + params, + ) + + threads = [ThreadData.model_validate(row[0]).thread for row in result.fetchall()] + + has_more = len(threads) > limit + if has_more: + threads = threads[:limit] + + next_after = threads[-1].id if (has_more and threads) else None + + return Page[ThreadMetadata]( + data=threads, + has_more=has_more, + after=next_after, + ) + + async def delete_thread(self, thread_id: str, context: RequestContext) -> None: + """Delete a thread and all its items.""" + async with self.session_factory() as session: + await session.execute( + text(f""" + DELETE FROM {self._get_table_name("items")} + WHERE thread_id = :thread_id AND user_id = :user_id + """), + {"thread_id": thread_id, "user_id": context.user_id}, + ) + + await session.execute( + text(f""" + DELETE FROM {self._get_table_name("threads")} + WHERE id = :thread_id AND user_id = :user_id + """), + {"thread_id": thread_id, "user_id": context.user_id}, + ) + + await session.commit() + + async def save_attachment(self, attachment: Attachment, context: RequestContext) -> None: + """Save attachment metadata.""" + attachment_data = AttachmentData(attachment=attachment) + + async with self.session_factory() as session: + await session.execute( + text(f""" + INSERT INTO {self._get_table_name("attachments")} + (id, user_id, organization_id, data) + VALUES (:id, :user_id, :org_id, :data) + ON CONFLICT (id, user_id) DO UPDATE SET + data = EXCLUDED.data + """), + { + "id": attachment.id, + "user_id": context.user_id, + "org_id": context.organization_id, + "data": attachment_data.model_dump_json(), + }, + ) + await session.commit() + + async def load_attachment(self, attachment_id: str, context: RequestContext) -> Attachment: + """Load attachment metadata.""" + async with self.session_factory() as session: + result = await session.execute( + text(f""" + SELECT data FROM {self._get_table_name("attachments")} + WHERE id = :attachment_id AND user_id = :user_id + """), + {"attachment_id": attachment_id, "user_id": context.user_id}, + ) + row = result.first() + + if not row: + raise NotFoundError(f"Attachment {attachment_id} not found") + + return AttachmentData.model_validate(row[0]).attachment + + async def delete_attachment(self, attachment_id: str, context: RequestContext) -> None: + """Delete attachment metadata.""" + async with self.session_factory() as session: + await session.execute( + text(f""" + DELETE FROM {self._get_table_name("attachments")} + WHERE id = :attachment_id AND user_id = :user_id + """), + {"attachment_id": attachment_id, "user_id": context.user_id}, + ) + await session.commit() + + async def close(self) -> None: + """Close the database connection pool.""" + await self.engine.dispose() + logger.info("PostgresStore connection pool closed") diff --git a/packages/api/src/taskflow_api/config.py b/packages/api/src/taskflow_api/config.py index 2a5ecc1..94ef152 100644 --- a/packages/api/src/taskflow_api/config.py +++ b/packages/api/src/taskflow_api/config.py @@ -1,5 +1,7 @@ """Application configuration from environment variables.""" +import os + from pydantic_settings import BaseSettings, SettingsConfigDict @@ -10,6 +12,7 @@ class Settings(BaseSettings): env_file=".env", env_file_encoding="utf-8", case_sensitive=False, + extra="ignore", # Ignore TASKFLOW_CHATKIT_* vars (handled by StoreConfig) ) # Database (required) @@ -31,10 +34,21 @@ class Settings(BaseSettings): dev_user_email: str = "dev@localhost" dev_user_name: str = "Dev User" + # MCP Server URL for TaskFlow tools (required for chat) + mcp_server_url: str = "http://localhost:8001/mcp" + + # OpenAI API Key (required for chat) + openai_api_key: str | None = None + @property def allowed_origins_list(self) -> list[str]: """Parse comma-separated origins into list.""" return [origin.strip() for origin in self.allowed_origins.split(",")] + @property + def chat_enabled(self) -> bool: + """Check if chat features are enabled (TASKFLOW_CHATKIT_DATABASE_URL set).""" + return os.getenv("TASKFLOW_CHATKIT_DATABASE_URL") is not None + settings = Settings() diff --git a/packages/api/src/taskflow_api/main.py b/packages/api/src/taskflow_api/main.py index b6708db..6975bc0 100644 --- a/packages/api/src/taskflow_api/main.py +++ b/packages/api/src/taskflow_api/main.py @@ -1,16 +1,23 @@ """TaskFlow API - Human-Agent Task Management Backend.""" +import json import logging from contextlib import asynccontextmanager from typing import Any +from chatkit.server import StreamingResult +from dotenv import load_dotenv from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, Response, StreamingResponse -from .config import settings -from .database import create_db_and_tables -from .routers import agents, audit, health, members, projects, tasks +# Load .env before anything else (for OPENAI_API_KEY used by Agents SDK) +load_dotenv() + +from .chatkit_store import RequestContext # noqa: E402 +from .config import settings # noqa: E402 +from .database import create_db_and_tables # noqa: E402 +from .routers import agents, audit, health, members, projects, tasks # noqa: E402 # Configure logging logging.basicConfig( @@ -31,10 +38,42 @@ async def lifespan(app: FastAPI): logger.info("SSO URL: %s", settings.sso_url) await create_db_and_tables() logger.info("Database initialized") + + # Initialize ChatKit store if configured (TASKFLOW_CHATKIT_DATABASE_URL) + if settings.chat_enabled: + logger.info("Chat enabled, initializing ChatKit store...") + logger.info("MCP Server URL: %s", settings.mcp_server_url) + + from .chatkit_store import PostgresStore, StoreConfig + from .services import create_chatkit_server + + try: + # StoreConfig reads TASKFLOW_CHATKIT_DATABASE_URL from env automatically + store_config = StoreConfig() + chatkit_store = PostgresStore(config=store_config) + await chatkit_store.initialize_schema() + app.state.chatkit_store = chatkit_store + app.state.chatkit_server = create_chatkit_server( + chatkit_store, + mcp_server_url=settings.mcp_server_url, + ) + logger.info("ChatKit store initialized successfully") + except Exception as e: + logger.error("Failed to initialize ChatKit store: %s", e) + logger.warning("Chat features will be unavailable") + else: + logger.info("Chat disabled (TASKFLOW_CHATKIT_DATABASE_URL not set)") + yield + # Shutdown logger.info("Shutting down TaskFlow API...") + # Cleanup ChatKit store + if hasattr(app.state, "chatkit_store"): + await app.state.chatkit_store.close() + logger.info("ChatKit store closed") + app = FastAPI( title="TaskFlow API", @@ -91,12 +130,100 @@ async def general_exception_handler(request: Request, exc: Exception) -> JSONRes app.include_router(audit.router, prefix="/api") +@app.post("/chatkit") +async def chatkit_endpoint(request: Request): + """ + Main ChatKit endpoint for conversational task management. + + Requires X-User-ID header for user identification. + Uses JWT auth when available, falls back to X-User-ID header. + """ + # Get server from app state + chatkit_server = getattr(request.app.state, "chatkit_server", None) + if not chatkit_server: + raise HTTPException( + status_code=503, + detail="ChatKit server not initialized. Check CHATKIT_DATABASE_URL configuration.", + ) + + # Extract user ID from header (ChatKit protocol uses X-User-ID) + user_id = request.headers.get("X-User-ID") + if not user_id: + raise HTTPException(status_code=401, detail="Missing X-User-ID header") + + # Extract JWT token from Authorization header (Bearer token) + auth_header = request.headers.get("Authorization") + access_token = None + if auth_header and auth_header.startswith("Bearer "): + access_token = auth_header[7:] # Remove "Bearer " prefix + + if not access_token: + raise HTTPException( + status_code=401, detail="Missing Authorization header with Bearer token" + ) + + try: + # Process ChatKit request + payload = await request.body() + + # Decode payload to extract metadata + payload_dict = json.loads(payload) + + # Extract metadata from the correct location in ChatKit request + metadata = {} + if "params" in payload_dict and "input" in payload_dict["params"]: + metadata = payload_dict["params"]["input"].get("metadata", {}) + + # Add access_token to metadata so ChatKit server can pass it to agent + metadata["access_token"] = access_token + + logger.debug("ChatKit request metadata keys: %s", list(metadata.keys())) + + # Create request context + context = RequestContext( + user_id=user_id, + request_id=request.headers.get("X-Request-ID"), + metadata=metadata, + ) + + result = await chatkit_server.process(payload, context) + + # Return appropriate response type + if isinstance(result, StreamingResult): + return StreamingResponse( + result, + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + else: + return Response( + content=result.json, + media_type="application/json", + ) + except json.JSONDecodeError as e: + logger.error("Invalid JSON in ChatKit request: %s", e) + raise HTTPException(status_code=400, detail="Invalid JSON payload") + except Exception as e: + logger.exception("Error processing ChatKit request: %s", e) + raise HTTPException(status_code=500, detail=f"Error processing request: {e!s}") + + @app.get("/") async def root() -> dict[str, Any]: """API root - returns basic info.""" + chatkit_status = ( + "active" + if hasattr(app.state, "chatkit_server") and app.state.chatkit_server + else "not configured" + ) return { "name": "TaskFlow API", "version": "1.0.0", "docs": "/docs", "health": "/health", + "chatkit": f"/chatkit ({chatkit_status})", } diff --git a/packages/api/src/taskflow_api/routers/__init__.py b/packages/api/src/taskflow_api/routers/__init__.py index c5cfac8..2678349 100644 --- a/packages/api/src/taskflow_api/routers/__init__.py +++ b/packages/api/src/taskflow_api/routers/__init__.py @@ -1 +1,12 @@ """FastAPI routers.""" + +from . import agents, audit, health, members, projects, tasks + +__all__ = [ + "agents", + "audit", + "health", + "members", + "projects", + "tasks", +] diff --git a/packages/api/src/taskflow_api/services/__init__.py b/packages/api/src/taskflow_api/services/__init__.py index de2060f..6e21ca9 100644 --- a/packages/api/src/taskflow_api/services/__init__.py +++ b/packages/api/src/taskflow_api/services/__init__.py @@ -1 +1,11 @@ """Business logic services.""" + +from .chat_agent import TASKFLOW_SYSTEM_PROMPT, create_taskflow_agent +from .chatkit_server import TaskFlowChatKitServer, create_chatkit_server + +__all__ = [ + "TASKFLOW_SYSTEM_PROMPT", + "create_taskflow_agent", + "TaskFlowChatKitServer", + "create_chatkit_server", +] diff --git a/packages/api/src/taskflow_api/services/chat_agent.py b/packages/api/src/taskflow_api/services/chat_agent.py new file mode 100644 index 0000000..8aca6b8 --- /dev/null +++ b/packages/api/src/taskflow_api/services/chat_agent.py @@ -0,0 +1,106 @@ +"""TaskFlow Agent configuration with MCP integration. + +This module provides the agent factory that creates an OpenAI Agents SDK agent +connected to the TaskFlow MCP Server via Streamable HTTP transport. + +The agent discovers tools dynamically from the MCP server - no function tools needed. +""" + +from agents import Agent +from agents.mcp import MCPServerStreamableHttp + +TASKFLOW_SYSTEM_PROMPT = """You are TaskFlow Assistant, an AI helper for task management. + +## Authentication Context +- User ID: {user_id} +- Access Token: {access_token} + +CRITICAL: When calling ANY MCP tool, you MUST ALWAYS include these parameters: +- user_id: "{user_id}" +- access_token: "{access_token}" + +## User Context +- User Name: {user_name} +- Current Project: {project_name} (ID: {project_id}) + +## Conversation History +{history} + +## Your Capabilities +Using the available MCP tools, you can: +- **Add tasks**: Create new tasks in the current project +- **List tasks**: Show all tasks, filter by status (pending, in_progress, completed) +- **Update tasks**: Modify task title, description, status, or assignment +- **Complete tasks**: Mark tasks as done +- **Delete tasks**: Remove tasks from the project +- **Assign tasks**: Assign tasks to team members or agents + +## Guidelines +1. Always confirm actions with the user after executing them +2. When listing tasks, format them clearly with ID, title, status, and assignee +3. If a request is ambiguous, ask for clarification +4. If a task is not found, suggest listing tasks to find the correct one +5. Be concise and helpful +6. ALWAYS include user_id="{user_id}" and access_token="{access_token}" in every tool call + +## Response Format +- For task lists, use a clear formatted list with key details +- For confirmations, briefly state what was done and the result +- For errors, explain what went wrong and suggest next steps +""" + + +async def create_taskflow_agent( + user_name: str, + project_name: str | None, + project_id: int | None, + mcp_server_url: str, + history: str = "", +) -> tuple[Agent, MCPServerStreamableHttp]: + """Create a TaskFlow agent with MCP server connection. + + The agent connects to the TaskFlow MCP Server via Streamable HTTP transport + and discovers available tools dynamically. + + Args: + user_name: Display name of the current user + project_name: Name of the current project context (optional) + project_id: ID of the current project (optional) + mcp_server_url: URL of the TaskFlow MCP Server (e.g., http://localhost:8001/mcp) + history: Conversation history string + + Returns: + Tuple of (agent, mcp_server) - caller must manage the MCP server context. + + Example: + ```python + async with MCPServerStreamableHttp(...) as mcp_server: + agent = Agent(name="TaskFlow Assistant", mcp_servers=[mcp_server]) + result = await Runner.run(agent, "Add a task to buy groceries") + ``` + """ + mcp_server = MCPServerStreamableHttp( + name="TaskFlow MCP", + params={ + "url": mcp_server_url, + "timeout": 30, + }, + cache_tools_list=True, + max_retry_attempts=3, + ) + + # Format the system prompt with user context + instructions = TASKFLOW_SYSTEM_PROMPT.format( + user_name=user_name, + project_name=project_name or "No project selected", + project_id=project_id or "N/A", + history=history or "No previous messages", + ) + + agent = Agent( + name="TaskFlow Assistant", + instructions=instructions, + mcp_servers=[mcp_server], # Tools discovered dynamically from MCP! + ) + + return agent, mcp_server diff --git a/packages/api/src/taskflow_api/services/chatkit_server.py b/packages/api/src/taskflow_api/services/chatkit_server.py new file mode 100644 index 0000000..ff72561 --- /dev/null +++ b/packages/api/src/taskflow_api/services/chatkit_server.py @@ -0,0 +1,233 @@ +"""ChatKit server integration for TaskFlow with MCP. + +This module provides the ChatKit server implementation that integrates +with the TaskFlow MCP Server for task management operations. +""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncIterator +from datetime import datetime + +from agents import Agent, Runner +from agents.mcp import MCPServerStreamableHttp +from chatkit.agents import AgentContext, stream_agent_response +from chatkit.server import ChatKitServer +from chatkit.types import ( + AssistantMessageContent, + AssistantMessageItem, + ThreadItemDoneEvent, + ThreadMetadata, + ThreadStreamEvent, + UserMessageItem, + UserMessageTextContent, +) + +from ..chatkit_store import PostgresStore, RequestContext +from .chat_agent import TASKFLOW_SYSTEM_PROMPT + +logger = logging.getLogger(__name__) + + +def _user_message_text(item: UserMessageItem | None) -> str: + """Extract text from user message item.""" + if not item: + return "" + parts: list[str] = [] + for part in item.content: + if isinstance(part, UserMessageTextContent): + parts.append(part.text) + return " ".join(parts).strip() + + +class TaskFlowAgentContext(AgentContext): + """Agent context for TaskFlow with store and request context.""" + + def __init__( + self, + thread: ThreadMetadata, + store: PostgresStore, + request_context: RequestContext, + ): + super().__init__(thread=thread, store=store, request_context=request_context) + + +class TaskFlowChatKitServer(ChatKitServer[RequestContext]): + """ + ChatKit server for TaskFlow task management with MCP integration. + + Integrates with the TaskFlow MCP Server to provide natural language + task management capabilities. Tools are discovered dynamically via MCP. + """ + + def __init__(self, store: PostgresStore, mcp_server_url: str): + """Initialize the ChatKit server with PostgreSQL store. + + Args: + store: PostgreSQL store for conversation persistence + mcp_server_url: URL of TaskFlow MCP Server (e.g., http://localhost:8001/mcp) + """ + super().__init__(store, attachment_store=None) + self.mcp_server_url = mcp_server_url + logger.info("TaskFlowChatKitServer initialized with MCP server: %s", mcp_server_url) + + async def respond( + self, + thread: ThreadMetadata, + input_user_message: UserMessageItem | None, + context: RequestContext, + ) -> AsyncIterator[ThreadStreamEvent]: + """ + Generate response for user message using TaskFlow agent with MCP. + + Args: + thread: Thread metadata + input_user_message: User's message (None for retry scenarios) + context: Request context with user_id and metadata + + Yields: + ThreadStreamEvent: Stream of chat events + """ + if not input_user_message: + logger.info("No user message provided - this is likely a read-only operation") + return + + try: + # Extract user message + user_text = _user_message_text(input_user_message) + if not user_text: + logger.warning("Empty user message") + return + + # Extract user info and auth token from context + user_id = context.user_id # From X-User-ID header + access_token = context.metadata.get("access_token", "") # From Authorization header + user_name = context.metadata.get("user_name") or context.user_id + project_name = context.metadata.get("project_name") + project_id = context.metadata.get("project_id") + + # Get previous messages from thread for context (last 20) + previous_items = await self.store.load_thread_items( + thread.id, + after=None, + limit=20, + order="desc", + context=context, + ) + + # Build message history for agent + messages = [] + for item in reversed(previous_items.data): + if isinstance(item, UserMessageItem): + messages.append({"role": "user", "content": _user_message_text(item)}) + elif isinstance(item, AssistantMessageItem): + messages.append( + { + "role": "assistant", + "content": item.content[0].text if item.content else "", + } + ) + + # Add current message + messages.append({"role": "user", "content": user_text}) + + # Create history string for agent prompt + history_str = "\n".join([f"{m['role']}: {m['content']}" for m in messages]) + + # Create agent context + agent_context = TaskFlowAgentContext( + thread=thread, + store=self.store, + request_context=context, + ) + + logger.info( + "Running TaskFlow agent for user %s with %d messages in history, MCP: %s", + context.user_id, + len(messages) - 1, + self.mcp_server_url, + ) + logger.debug( + "Auth context - user_id: %s, access_token present: %s", + user_id, + bool(access_token), + ) + + # Connect to MCP server and run agent + async with MCPServerStreamableHttp( + name="TaskFlow MCP", + params={ + "url": self.mcp_server_url, + "timeout": 30, + }, + cache_tools_list=True, + max_retry_attempts=3, + ) as mcp_server: + # Format system prompt with user context and auth token + instructions = TASKFLOW_SYSTEM_PROMPT.format( + user_id=user_id, + access_token=access_token, + user_name=user_name, + project_name=project_name or "No project selected", + project_id=project_id or "N/A", + history=history_str, + ) + + agent = Agent( + name="TaskFlow Assistant", + instructions=instructions, + mcp_servers=[mcp_server], # Tools discovered from MCP! + ) + + # Run agent with streaming + result = Runner.run_streamed(agent, user_text, context=agent_context) + async for event in stream_agent_response(agent_context, result): + yield event + + logger.info("TaskFlow agent response completed for user %s", context.user_id) + + except ConnectionError as e: + logger.error("MCP server connection failed: %s", e) + error_message = AssistantMessageItem( + id=self.store.generate_item_id("message", thread, context), + thread_id=thread.id, + created_at=datetime.now(), + content=[ + AssistantMessageContent( + text="I'm having trouble connecting to the task management service. " + "Please try again in a moment, or check if the MCP server is running.", + annotations=[], + ) + ], + ) + yield ThreadItemDoneEvent(item=error_message) + + except Exception as e: + logger.exception("Error in TaskFlow agent: %s", e) + error_message = AssistantMessageItem( + id=self.store.generate_item_id("message", thread, context), + thread_id=thread.id, + created_at=datetime.now(), + content=[ + AssistantMessageContent( + text="I apologize, but I encountered an error processing your request. " + "Please try again.", + annotations=[], + ) + ], + ) + yield ThreadItemDoneEvent(item=error_message) + + +def create_chatkit_server(store: PostgresStore, mcp_server_url: str) -> TaskFlowChatKitServer: + """Create a configured TaskFlow ChatKit server instance. + + Args: + store: PostgreSQL store for conversation persistence + mcp_server_url: URL of TaskFlow MCP Server + + Returns: + Configured TaskFlowChatKitServer instance + """ + return TaskFlowChatKitServer(store, mcp_server_url) diff --git a/packages/api/uv.lock b/packages/api/uv.lock index dfbbbe9..687adca 100644 --- a/packages/api/uv.lock +++ b/packages/api/uv.lock @@ -76,6 +76,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -130,6 +139,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -207,6 +257,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "ecdsa" version = "0.19.1" @@ -265,6 +324,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, ] +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -324,6 +395,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -342,6 +422,226 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "1.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/42/10c0c09ca27aceacd8c428956cfabdd67e3d328fe55c4abc16589285d294/mcp-1.23.1.tar.gz", hash = "sha256:7403e053e8e2283b1e6ae631423cb54736933fea70b32422152e6064556cd298", size = 596519, upload-time = "2025-12-02T18:41:12.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/9e/26e1d2d2c6afe15dfba5ca6799eeeea7656dce625c22766e4c57305e9cc2/mcp-1.23.1-py3-none-any.whl", hash = "sha256:3ce897fcc20a41bd50b4c58d3aa88085f11f505dcc0eaed48930012d34c731d8", size = 231433, upload-time = "2025-12-02T18:41:11.195Z" }, +] + +[[package]] +name = "openai" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/516290f38745cc1e72856f50e8afed4a7f9ac396a5a18f39e892ab89dfc2/openai-2.9.0.tar.gz", hash = "sha256:b52ec65727fc8f1eed2fbc86c8eac0998900c7ef63aa2eb5c24b69717c56fa5f", size = 608202, upload-time = "2025-12-04T18:15:09.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/fd/ae2da789cd923dd033c99b8d544071a827c92046b150db01cfa5cea5b3fd/openai-2.9.0-py3-none-any.whl", hash = "sha256:0d168a490fbb45630ad508a6f3022013c155a68fd708069b6a1a01a5e8f0ffad", size = 1030836, upload-time = "2025-12-04T18:15:07.063Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/6d/824b78c3161e0204f6fc71b459af64d8c65325a5486bc41b8eedcca7a715/openai_agents-0.6.2.tar.gz", hash = "sha256:1012aee224518292778fb4b07eb9148b0b5efa5cd87fd32ec656296aba885612", size = 2014662, upload-time = "2025-12-04T22:37:28.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0a/43e24985d9df314d3dfa5f004443e8a15ef2bdcc79718dc74ded5545bf7d/openai_agents-0.6.2-py3-none-any.whl", hash = "sha256:3156637f3eee925268943f5c4b500b3b22b179158b82e7b53ed7ab362edf84d6", size = 238302, upload-time = "2025-12-04T22:37:26.313Z" }, +] + +[[package]] +name = "openai-chatkit" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "openai" }, + { name = "openai-agents" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/89/bf2f094997c8e5cad5334e8a02e05fc458823e65fb2675f45b56b6d1ab73/openai_chatkit-1.4.0.tar.gz", hash = "sha256:e2527dffc3794a05596ad75efa66bdc4efb4ded5a77a013a55496cc989bcf2e6", size = 55269, upload-time = "2025-11-25T21:02:58.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/bf/68d42561dd8a674b6f8541d879dd165b5ac4d81fcf1027462e154de66a4f/openai_chatkit-1.4.0-py3-none-any.whl", hash = "sha256:35d00ca8398908bd70d63e2284adcd836641cc11746f68d7cfa91d276e3dad3d", size = 39077, upload-time = "2025-11-25T21:02:57.288Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -469,6 +769,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -537,6 +851,28 @@ cryptography = [ { name = "cryptography" }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -573,6 +909,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + [[package]] name = "rsa" version = "4.9.1" @@ -620,6 +1050,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.44" @@ -659,6 +1098,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + [[package]] name = "starlette" version = "0.50.0" @@ -679,8 +1130,11 @@ dependencies = [ { name = "asyncpg" }, { name = "fastapi" }, { name = "httpx" }, + { name = "openai-agents" }, + { name = "openai-chatkit" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-dotenv" }, { name = "python-jose", extra = ["cryptography"] }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "sqlmodel" }, @@ -706,11 +1160,14 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.30.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "openai-agents", specifier = ">=0.0.9" }, + { name = "openai-chatkit", specifier = ">=1.4.0" }, { name = "pydantic", specifier = ">=2.10.0" }, { name = "pydantic-settings", specifier = ">=2.6.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.0" }, { name = "sqlmodel", specifier = ">=0.0.22" }, @@ -721,6 +1178,30 @@ provides-extras = ["dev"] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.14.8" }] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -742,6 +1223,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" }, +] + [[package]] name = "uvicorn" version = "0.38.0" diff --git a/specs/006-chat-server/checklists/requirements.md b/specs/006-chat-server/checklists/requirements.md new file mode 100644 index 0000000..dfca834 --- /dev/null +++ b/specs/006-chat-server/checklists/requirements.md @@ -0,0 +1,69 @@ +# Requirements Checklist: 006-chat-server + +**Spec File**: `specs/006-chat-server/spec.md` +**Generated**: 2025-12-07 +**Validation Status**: READY + +## Content Quality + +- [x] No implementation details (frameworks, languages, databases) +- [x] User-focused language throughout +- [x] Business value clearly articulated +- [x] Technology-agnostic success criteria + +## Requirement Completeness + +- [x] All requirements are testable and falsifiable +- [x] Requirements use MUST/SHOULD/MAY appropriately +- [x] No ambiguous terms without definition +- [x] Edge cases identified and addressed +- [x] Error scenarios documented + +## Feature Readiness + +- [x] User scenarios cover primary workflows +- [x] Acceptance criteria are specific and measurable +- [x] Scope boundaries clearly defined (Non-Goals section) +- [x] Dependencies identified +- [x] Assumptions documented + +## Constitutional Alignment (TaskFlow-Specific) + +- [x] **Audit Principle**: FR-010 ensures all chat-initiated operations create audit entries +- [x] **Agent Parity**: Chat operations use same MCP tools available to AI agents +- [x] **Spec-Driven**: Specification created before implementation +- [x] **Phase Continuity**: Conversation/Message models follow SQLModel patterns + +## Traceability + +| Requirement | User Story | Success Criteria | +|-------------|------------|------------------| +| FR-001 | US1, US2, US3, US4 | SC-001 | +| FR-002 | All | - | +| FR-003 | US5 | SC-003 | +| FR-004 | US1, US2, US3, US4 | SC-002 | +| FR-005 | US5 | SC-003 | +| FR-006 | US5 | SC-003 | +| FR-007 | US1, US3 | SC-004 | +| FR-008 | US1 | SC-002 | +| FR-009 | - | - | +| FR-010 | All | SC-005 | + +## Validation Summary + +**Overall Readiness**: READY + +All checklist items pass. The specification is ready for planning phase. + +### Notes + +1. No [NEEDS CLARIFICATION] markers - all requirements have reasonable defaults based on: + - Existing rag-agent ChatKit implementation patterns + - Hackathon Phase III requirements document + - TaskFlow constitutional principles + +2. Key design decisions made with informed defaults: + - Conversation history limit: 20 messages (standard ChatKit practice) + - MCP transport: HTTP (as specified in user input) + - Database: Separate CHATKIT_STORE_DATABASE_URL (user-specified requirement) + - Auth: JWT/JWKS (matches existing API patterns) diff --git a/specs/006-chat-server/plan.md b/specs/006-chat-server/plan.md new file mode 100644 index 0000000..b65dbe2 --- /dev/null +++ b/specs/006-chat-server/plan.md @@ -0,0 +1,493 @@ +# Implementation Plan: TaskFlow Chat Server + +**Feature**: 006-chat-server +**Spec**: `specs/006-chat-server/spec.md` +**Created**: 2025-12-07 +**Status**: Ready for Implementation + +## Technical Context + +### Strategy: Reuse Existing Infrastructure + Focus on Agent + +**Key Insight**: We already have working ChatKit infrastructure in `rag-agent/`. We will: +1. **Copy** the `chatkit_store/` module (proven, production-ready) +2. **Focus** on building the TaskFlow-specific agent with `@function_tool` decorators +3. **MCP Optional**: Agent works standalone; MCP integration added when server is ready + +### Existing Infrastructure to REUSE + +| Component | Location | Reuse Strategy | +|-----------|----------|----------------| +| FastAPI App | `packages/api/src/taskflow_api/main.py` | Extend with chat router | +| JWT/JWKS Auth | `packages/api/src/taskflow_api/auth.py` | Reuse `get_current_user` | +| Database Session | `packages/api/src/taskflow_api/database.py` | Use for task operations | +| **ChatKit Store** | `rag-agent/chatkit_store/` | **Copy entire module** (separate DB) | +| ChatKit Patterns | `rag-agent/chatkit_server.py` | Adapt respond() pattern | + +### OpenAI Agents SDK - Key Classes (from Context7) + +```python +# Agent with function tools (works WITHOUT MCP) +from agents import Agent, Runner, function_tool + +@function_tool +def add_task(title: str, project_id: int) -> dict: + """Add a new task to the project.""" + # Calls existing TaskFlow API/database directly + ... + +agent = Agent( + name="TaskFlow Assistant", + instructions=SYSTEM_PROMPT, + tools=[add_task, list_tasks, complete_task, ...] # Function tools +) + +result = await Runner.run(agent, user_message) +``` + +```python +# Agent WITH MCP (when MCP server is ready) +from agents.mcp import MCPServerStreamableHttp + +async with MCPServerStreamableHttp( + name="TaskFlow MCP", + params={ + "url": "http://localhost:8001/mcp", + "timeout": 30, + }, +) as mcp_server: + agent = Agent( + name="TaskFlow Assistant", + instructions=SYSTEM_PROMPT, + mcp_servers=[mcp_server], # MCP tools discovered dynamically + ) +``` + +### Key Dependencies + +```toml +# Add to packages/api/pyproject.toml +[project.dependencies] +openai-agents = ">=0.0.9" +chatkit = ">=0.1.0" +# httpx already present for auth +``` + +## Constitution Check + +| Principle | Status | Implementation | +|-----------|--------|----------------| +| Audit Trail | ✓ | Function tools call audit service directly | +| Agent Parity | ✓ | Same operations available via CLI/Web/Chat | +| Recursive Tasks | ✓ | add_subtask function tool available | +| Spec-Driven | ✓ | Spec exists at specs/006-chat-server/spec.md | +| Phase Continuity | ✓ | ChatKit uses separate DB; Task model unchanged | + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ packages/api │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ FastAPI Application ││ +│ │ ┌───────────────┐ ┌───────────────┐ ┌─────────────────┐ ││ +│ │ │ /api/projects │ │ /api/tasks │ │ /api/chat │ ││ +│ │ │ /api/workers │ │ /api/audit │ │ (NEW) │ ││ +│ │ └───────────────┘ └───────────────┘ └────────┬────────┘ ││ +│ └─────────────────────────────────────────────────┼──────────┘│ +│ │ │ +│ ┌─────────────────────────────────────────────────▼──────────┐│ +│ │ Chat Service Layer ││ +│ │ ┌────────────────────┐ ┌────────────────────────────────┐││ +│ │ │ ChatKit Store │ │ TaskFlow Agent │││ +│ │ │ (PostgresStore) │ │ (OpenAI Agents SDK) │││ +│ │ │ [COPY from │ │ [@function_tool decorators] │││ +│ │ │ rag-agent] │ │ │││ +│ │ └────────────────────┘ └────────────────────────────────┘││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (OPTIONAL - Phase 2) +┌─────────────────────────────────────────────────────────────────┐ +│ TaskFlow MCP Server │ +│ (MCPServerStreamableHttp when ready) │ +│ URL: http://localhost:8001/mcp (Streamable HTTP transport) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Implementation Phases + +### Phase 1: Copy ChatKit Store Module (15 min) + +**Goal**: Bring proven chatkit_store module into packages/api + +**Files to Copy from `rag-agent/chatkit_store/`**: +``` +packages/api/src/taskflow_api/ +├── chatkit_store/ +│ ├── __init__.py # Copy as-is +│ ├── config.py # Update prefix to TASKFLOW_CHATKIT_ +│ ├── context.py # Copy as-is +│ └── postgres_store.py # Copy as-is +``` + +**Modifications**: +- `config.py`: Change `env_prefix="CHATKIT_STORE_"` → `env_prefix="TASKFLOW_CHATKIT_"` +- `config.py`: Change `schema_name: str = "chatkit"` → `schema_name: str = "taskflow_chat"` + +### Phase 2: Create TaskFlow Agent with MCP (30 min) ⭐ CORE WORK + +**Goal**: Configure Agent that connects to TaskFlow MCP Server via Streamable HTTP + +**Files to Create**: +``` +packages/api/src/taskflow_api/ +├── services/ +│ └── chat_agent.py # Agent factory with MCP connection +``` + +**Agent with MCP** (tools discovered dynamically from MCP server): +```python +from agents import Agent, Runner +from agents.mcp import MCPServerStreamableHttp + +TASKFLOW_SYSTEM_PROMPT = """You are TaskFlow Assistant, helping users manage their tasks. + +User: {user_name} +Current Project: {project_name} (ID: {project_id}) + +You can use the available tools to: +- Add, list, update, delete tasks +- Mark tasks complete +- Assign tasks to team members or agents +- Track progress + +Always confirm actions with the user. Be concise and helpful. +When showing task lists, format them clearly with ID, title, status, and assignee. +""" + +async def create_taskflow_agent( + user_name: str, + project_name: str, + project_id: int, + mcp_server_url: str, +) -> tuple[Agent, MCPServerStreamableHttp]: + """Create agent with MCP server connection. + + Returns tuple of (agent, mcp_server) - caller must manage context. + """ + mcp_server = MCPServerStreamableHttp( + name="TaskFlow MCP", + params={ + "url": mcp_server_url, # http://localhost:8001/mcp + "timeout": 30, + }, + cache_tools_list=True, + max_retry_attempts=3, + ) + + agent = Agent( + name="TaskFlow Assistant", + instructions=TASKFLOW_SYSTEM_PROMPT.format( + user_name=user_name, + project_name=project_name, + project_id=project_id, + ), + mcp_servers=[mcp_server], # Tools discovered from MCP! + ) + + return agent, mcp_server +``` + +**Key Point**: No `@function_tool` layer needed! MCP server exposes tools like: +- `taskflow_add_task` +- `taskflow_list_tasks` +- `taskflow_complete_task` +- `taskflow_update_task` +- `taskflow_delete_task` + +Agent discovers these dynamically via MCP protocol. + +### Phase 3: Create ChatKit Server Adapter (30 min) + +**Goal**: Adapt ChatKit server pattern for TaskFlow with MCP integration + +**Files to Create**: +``` +packages/api/src/taskflow_api/ +├── services/ +│ └── chatkit_server.py # TaskFlowChatKitServer class +``` + +**Key Implementation** (adapted from `rag-agent/chatkit_server.py`): +```python +from chatkit.server import ChatKitServer +from chatkit.agents import stream_agent_response +from agents.mcp import MCPServerStreamableHttp + +class TaskFlowChatKitServer(ChatKitServer[RequestContext]): + """ChatKit server for TaskFlow task management with MCP.""" + + def __init__(self, store, mcp_server_url: str): + super().__init__(store) + self.mcp_server_url = mcp_server_url + + async def respond( + self, + thread: ThreadMetadata, + input_user_message: UserMessageItem | None, + context: RequestContext, + ) -> AsyncIterator[ThreadStreamEvent]: + # 1. Extract user message + user_text = _user_message_text(input_user_message) + + # 2. Load conversation history (last 20 messages) + previous_items = await self.store.load_thread_items(...) + history_str = "\n".join([...]) + + # 3. Create agent WITH MCP connection + async with MCPServerStreamableHttp( + name="TaskFlow MCP", + params={"url": self.mcp_server_url, "timeout": 30}, + cache_tools_list=True, + ) as mcp_server: + agent = Agent( + name="TaskFlow Assistant", + instructions=SYSTEM_PROMPT.format( + user_name=context.metadata.get("user_name"), + project_name=context.metadata.get("project_name"), + project_id=context.metadata.get("project_id"), + history=history_str, + ), + mcp_servers=[mcp_server], # Tools from MCP! + ) + + # 4. Run agent and stream response + result = Runner.run_streamed(agent, user_text) + async for event in stream_agent_response(agent_context, result): + yield event +``` + +### Phase 4: Create Chat Router + Schemas (30 min) + +**Goal**: Expose POST /api/chat endpoint with proper schemas + +**Files to Create**: +``` +packages/api/src/taskflow_api/ +├── schemas/ +│ └── chat.py # Request/Response schemas +├── routers/ +│ └── chat.py # POST /api/chat endpoint +``` + +**Schemas**: +```python +class ChatRequest(BaseModel): + conversation_id: str | None = None # ChatKit thread_id + message: str + project_id: int | None = None + +class ToolCallResult(BaseModel): + tool: str + args: dict[str, Any] + result: dict[str, Any] + +class ChatResponse(BaseModel): + conversation_id: str + response: str + tool_calls: list[ToolCallResult] +``` + +**Endpoint**: +```python +@router.post("/chat", response_model=ChatResponse) +async def chat( + request: ChatRequest, + user: CurrentUser = Depends(get_current_user), +): + chatkit_server = request.app.state.chatkit_server + + # Build context with JWT user info + context = RequestContext( + user_id=user.id, + metadata={ + "user_name": user.name, + "project_id": request.project_id, + } + ) + + # Process through ChatKit + result = await chatkit_server.process(payload, context) + return result +``` + +### Phase 5: Wire Up in Main App (15 min) + +**Goal**: Initialize ChatKit store in app lifespan + +**Files to Modify**: +``` +packages/api/src/taskflow_api/ +├── main.py # Add ChatKit lifespan +├── config.py # Add new settings +``` + +**Config Additions**: +```python +class Settings(BaseSettings): + # Existing... + + # ChatKit (separate database) + chatkit_database_url: str | None = None # Falls back to database_url + + # MCP Server (required for chat) + mcp_server_url: str = "http://localhost:8001/mcp" + + # OpenAI + openai_api_key: str +``` + +**Lifespan**: +```python +@asynccontextmanager +async def lifespan(app: FastAPI): + # Existing startup... + + # Initialize ChatKit store + chatkit_db_url = settings.chatkit_database_url or settings.database_url + if chatkit_db_url: + store_config = StoreConfig(database_url=chatkit_db_url) + chatkit_store = PostgresStore(config=store_config) + await chatkit_store.initialize_schema() + app.state.chatkit_store = chatkit_store + app.state.chatkit_server = TaskFlowChatKitServer( + chatkit_store, + mcp_server_url=settings.mcp_server_url, + ) + + yield + + # Cleanup + if hasattr(app.state, "chatkit_store"): + await app.state.chatkit_store.close() +``` + +### Phase 6: Testing (20 min) + +**Goal**: Verify agent works end-to-end with MCP + +**Test Cases**: +1. Chat with MCP tools - "Add a task called Buy groceries" +2. List tasks via chat - "Show me my tasks" +3. Conversation persistence - Thread ID persists across requests +4. Auth required - 401 without JWT +5. MCP connection error - Graceful failure message + +## File Summary + +### New Files (7 files) + +| File | Purpose | Lines (Est.) | +|------|---------|--------------| +| `chatkit_store/__init__.py` | Module exports (copy from rag-agent) | 20 | +| `chatkit_store/config.py` | Store configuration (modify prefix) | 60 | +| `chatkit_store/context.py` | Request context (copy as-is) | 30 | +| `chatkit_store/postgres_store.py` | PostgreSQL store (copy as-is) | 400 | +| `schemas/chat.py` | Request/Response schemas | 40 | +| `services/chatkit_server.py` | **ChatKit server with MCP** | 200 | +| `routers/chat.py` | POST /api/chat endpoint | 80 | + +### Modified Files (3 files) + +| File | Changes | +|------|---------| +| `main.py` | Add ChatKit lifespan, include chat router | +| `config.py` | Add chatkit_database_url, mcp_server_url, openai_api_key | +| `pyproject.toml` | Add openai-agents, chatkit dependencies | + +## Configuration + +### Environment Variables + +```bash +# ChatKit Store (separate from main database - can use same DB, different schema) +TASKFLOW_CHATKIT_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/taskflow + +# MCP Server (Streamable HTTP transport) +MCP_SERVER_URL=http://localhost:8001/mcp + +# OpenAI API (required) +OPENAI_API_KEY=sk-... + +# Existing +DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/taskflow +SSO_URL=http://localhost:3001 +``` + +## Dependency Graph + +``` +Phase 1 (chatkit_store copy) ────────────────┐ + │ +Phase 2 (chat_agent.py) ⭐ ──────────────────┤ + [Agent + MCPServerStreamableHttp] │ + ▼ +Phase 3 (chatkit_server.py) ─────────► Phase 4 (chat.py router + schemas) + [ChatKit + MCP integration] │ + ▼ + Phase 5 (main.py integration) + │ + ▼ + Phase 6 (tests) +``` + +## MCP Integration (Direct - No Function Tools Layer) + +Agent connects directly to TaskFlow MCP Server via Streamable HTTP: + +```python +from agents.mcp import MCPServerStreamableHttp + +async with MCPServerStreamableHttp( + name="TaskFlow MCP", + params={ + "url": "http://localhost:8001/mcp", + "timeout": 30, + }, + cache_tools_list=True, + max_retry_attempts=3, +) as mcp_server: + agent = Agent( + name="TaskFlow Assistant", + instructions=system_prompt, + mcp_servers=[mcp_server], # Tools discovered dynamically! + ) +``` + +**Benefits**: +- Tools defined once in MCP server, used by CLI/Web/Chat/Agents +- Dynamic tool discovery (no code changes needed when tools added) +- Standardized protocol for agent-to-service communication +- No duplicate `@function_tool` implementations + +## Risk Mitigation + +| Risk | Mitigation | +|------|------------| +| MCP Server not running | Return friendly error, log for debugging | +| OpenAI API rate limits | Log warnings, return error to user | +| ChatKit store connection issues | Separate schema, main API still works | +| Tool call failures | Catch exceptions, inform user of partial failure | + +## Success Validation + +After implementation: + +1. [ ] `POST /api/chat` returns valid response +2. [ ] Task created via chat appears in `GET /api/tasks` +3. [ ] Conversation persists across requests +4. [ ] JWT auth required for chat endpoint +5. [ ] Tool calls visible in response (from MCP) +6. [ ] MCP connection error returns friendly message +7. [ ] All tests pass (`uv run pytest`) diff --git a/specs/006-chat-server/spec.md b/specs/006-chat-server/spec.md new file mode 100644 index 0000000..c94fcbe --- /dev/null +++ b/specs/006-chat-server/spec.md @@ -0,0 +1,180 @@ +# Feature Specification: TaskFlow Chat Server + +**Feature Branch**: `006-chat-server` +**Created**: 2025-12-07 +**Status**: Draft +**Input**: User description: "Build ChatKit server in existing FastAPI packages/api with OpenAI Agents SDK, MCP integration, conversation persistence, and JWT auth" + +## Overview + +The TaskFlow Chat Server enables natural language task management through a conversational interface. Users interact with an AI assistant that can create, list, update, delete, and complete tasks using natural language commands. The chat server integrates with the existing TaskFlow API and connects to the TaskFlow MCP Server for tool execution. + +### Context + +- **Phase**: III (MCP + Chat) of TaskFlow Hackathon +- **Integration Point**: Extends existing `packages/api` FastAPI application +- **Reference Implementation**: `rag-agent/` provides working ChatKit patterns to reuse +- **MCP Server**: TaskFlow MCP Server (separate service, HTTP transport on port 8001) + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Natural Language Task Creation (Priority: P1) + +A user opens the chat interface and says "Add a task to buy groceries". The AI assistant creates the task in their current project and confirms the action. + +**Why this priority**: Core value proposition - users can manage tasks without navigating forms or learning CLI commands. + +**Independent Test**: Can be fully tested by sending a chat message and verifying task appears in task list. + +**Acceptance Scenarios**: + +1. **Given** a user is authenticated and has a project, **When** they send "Add a task to buy groceries", **Then** a new task titled "Buy groceries" is created in their project and the assistant confirms with task details. + +2. **Given** a user sends a task creation request, **When** they provide additional context like "Add task to review PR by tomorrow, high priority", **Then** the task is created with the specified priority and due date. + +3. **Given** a user has no project context set, **When** they try to add a task, **Then** the assistant prompts them to specify or select a project. + +--- + +### User Story 2 - Task Listing and Status Queries (Priority: P1) + +A user asks "What's on my plate?" or "Show me pending tasks" and receives a formatted list of their tasks with status, priority, and assignment information. + +**Why this priority**: Users need visibility into their work alongside the ability to create tasks. + +**Independent Test**: Can be tested by querying tasks and verifying response matches database state. + +**Acceptance Scenarios**: + +1. **Given** a user has tasks in their project, **When** they ask "Show me all my tasks", **Then** they receive a list of all tasks with title, status, and assignee. + +2. **Given** a user asks "What's pending?", **When** the query is processed, **Then** only tasks with status "pending" are returned. + +3. **Given** a user asks "What have I completed?", **When** the query is processed, **Then** only tasks with status "completed" are returned. + +--- + +### User Story 3 - Task Completion and Updates (Priority: P2) + +A user says "Mark task 3 as complete" or "Change task 1 to 'Call mom tonight'" and the assistant updates the task accordingly. + +**Why this priority**: Task lifecycle management is essential but depends on having tasks created first. + +**Independent Test**: Can be tested by modifying existing tasks and verifying database updates. + +**Acceptance Scenarios**: + +1. **Given** a task exists with ID 3, **When** user says "Mark task 3 as complete", **Then** the task status changes to "completed" and assistant confirms. + +2. **Given** a task exists with ID 1, **When** user says "Change task 1 to 'Call mom tonight'", **Then** the task title is updated and assistant confirms the change. + +3. **Given** a user references a non-existent task, **When** they try to update it, **Then** the assistant explains the task was not found and suggests listing tasks. + +--- + +### User Story 4 - Task Deletion (Priority: P2) + +A user says "Delete the meeting task" and the assistant identifies and removes the task after confirmation. + +**Why this priority**: Cleanup operations are important but less frequent than creation and updates. + +**Independent Test**: Can be tested by deleting a task and verifying removal. + +**Acceptance Scenarios**: + +1. **Given** a task titled "meeting" exists, **When** user says "Delete the meeting task", **Then** the assistant identifies the task, confirms deletion, and removes it. + +2. **Given** multiple tasks match "meeting", **When** user requests deletion, **Then** the assistant lists matching tasks and asks for clarification. + +--- + +### User Story 5 - Conversation Continuity (Priority: P3) + +A user returns to the chat after closing their browser and continues their previous conversation with context preserved. + +**Why this priority**: Session persistence improves UX but core functionality works without it. + +**Independent Test**: Can be tested by creating conversation, closing session, and resuming. + +**Acceptance Scenarios**: + +1. **Given** a user has previous chat history, **When** they open a new session with the same conversation_id, **Then** previous messages are loaded and context is preserved. + +2. **Given** a user starts fresh, **When** they don't provide a conversation_id, **Then** a new conversation is created. + +--- + +### Edge Cases + +- What happens when the MCP server is unavailable? → Return friendly error message suggesting retry. +- How does the system handle ambiguous task references like "the task"? → List matching tasks and ask for clarification. +- What happens when a user exceeds conversation history limit? → Oldest messages are truncated, most recent 20 retained. +- What happens when the user is not authenticated? → Return 401 Unauthorized. +- What happens when OpenAI API fails? → Log error and return generic error message to user. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST provide a POST /api/chat endpoint that accepts user messages and returns AI responses. +- **FR-002**: System MUST authenticate users via existing JWT/JWKS mechanism (same as other API endpoints). +- **FR-003**: System MUST persist conversations and messages to the database. +- **FR-004**: System MUST connect to TaskFlow MCP Server via HTTP transport to execute task operations. +- **FR-005**: System MUST include conversation history (last 20 messages) when generating AI responses. +- **FR-006**: System MUST support creating new conversations when no conversation_id is provided. +- **FR-007**: System MUST return tool_calls in the response showing which MCP tools were invoked. +- **FR-008**: System MUST inject user context (name, current project) into the AI agent's system prompt. +- **FR-009**: System MUST use a separate database connection (CHATKIT_STORE_DATABASE_URL) for ChatKit persistence. +- **FR-010**: System MUST create audit log entries for all task operations performed through chat. + +### Key Entities + +- **Conversation**: Represents a chat session. Contains user_id, optional project_id for context, and timestamps. One user can have multiple conversations. + +- **Message**: A single message within a conversation. Contains role (user/assistant), content text, optional tool_calls (JSON array of invoked tools), and timestamp. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Users can create a task through natural language chat in under 5 seconds. +- **SC-002**: System correctly interprets and executes 90% of task management requests on first attempt. +- **SC-003**: Conversation history persists across browser sessions with no data loss. +- **SC-004**: Chat responses include confirmation of actions taken with specific task details. +- **SC-005**: All chat-initiated task operations appear in the audit trail with actor identified. +- **SC-006**: System handles MCP server unavailability gracefully with user-friendly error messages. + +## Assumptions + +1. **MCP Server Availability**: The TaskFlow MCP Server will be running on a configurable URL (default: http://localhost:8001). +2. **Database Configuration**: CHATKIT_STORE_DATABASE_URL environment variable will be set for ChatKit's separate database connection. +3. **OpenAI API Key**: OPENAI_API_KEY environment variable will be available for Agents SDK. +4. **User Context**: Users will have at least one project to work with; project_id can be passed in chat requests. +5. **ChatKit Infrastructure**: Reuse existing chatkit_store patterns from rag-agent for PostgreSQL persistence. + +## Non-Goals + +- Voice input/output (text-only interface) +- Multi-language support beyond English +- Real-time streaming of partial responses (initial implementation returns complete responses) +- Direct database access from chat (all operations go through MCP tools) +- Agent-to-agent chat delegation + +## Dependencies + +- OpenAI Agents SDK (`openai-agents`) +- ChatKit server library (`chatkit`) +- Existing TaskFlow API authentication (JWT/JWKS) +- TaskFlow MCP Server (HTTP transport) +- PostgreSQL database for conversation storage + +## Agent Behavior Reference + +| User Says | Agent Action | +|-----------|--------------| +| "Add a task to buy groceries" | Call taskflow_add_task | +| "Show me all my tasks" | Call taskflow_list_tasks with status "all" | +| "What's pending?" | Call taskflow_list_tasks with status "pending" | +| "Mark task 3 as complete" | Call taskflow_complete_task | +| "Delete the meeting task" | Call taskflow_list_tasks first, then taskflow_delete_task | +| "Change task 1 to 'Call mom tonight'" | Call taskflow_update_task | diff --git a/specs/main/plan.md b/specs/main/plan.md new file mode 100644 index 0000000..b420402 --- /dev/null +++ b/specs/main/plan.md @@ -0,0 +1,104 @@ +# Implementation Plan: [FEATURE] + +**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` + +**Note**: This template is filled in by the `/sp.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +[Extract from feature spec: primary requirement + technical approach from research] + +## Technical Context + + + +**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] +**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] +**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] +**Project Type**: [single/web/mobile - determines source structure] +**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] +**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] +**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +[Gates determined based on constitution file] + +## Project Structure + +### Documentation (this feature) + +```text +specs/[###-feature]/ +├── plan.md # This file (/sp.plan command output) +├── research.md # Phase 0 output (/sp.plan command) +├── data-model.md # Phase 1 output (/sp.plan command) +├── quickstart.md # Phase 1 output (/sp.plan command) +├── contracts/ # Phase 1 output (/sp.plan command) +└── tasks.md # Phase 2 output (/sp.tasks command - NOT created by /sp.plan) +``` + +### Source Code (repository root) + + +```text +# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) +src/ +├── models/ +├── services/ +├── cli/ +└── lib/ + +tests/ +├── contract/ +├── integration/ +└── unit/ + +# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) +backend/ +├── src/ +│ ├── models/ +│ ├── services/ +│ └── api/ +└── tests/ + +frontend/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ └── services/ +└── tests/ + +# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) +api/ +└── [same as backend above] + +ios/ or android/ +└── [platform-specific structure: feature modules, UI flows, platform tests] +``` + +**Structure Decision**: [Document the selected structure and reference the real +directories captured above] + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | +| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |