tiphys.main

Tiphys entry point.

Provides the FastAPI application and CLI interface.

  1"""
  2Tiphys entry point.
  3
  4Provides the FastAPI application and CLI interface.
  5"""
  6
  7from collections.abc import AsyncIterator
  8from contextlib import asynccontextmanager
  9from datetime import datetime
 10from pathlib import Path
 11from typing import TYPE_CHECKING
 12
 13import structlog
 14from fastapi import FastAPI
 15from fastapi.middleware.cors import CORSMiddleware
 16from fastapi.responses import FileResponse
 17from fastapi.staticfiles import StaticFiles
 18
 19from tiphys.bootstrap import create_app_context
 20from tiphys.config import TiphysConfig, load_config
 21from tiphys.context import AppContext, get_context
 22from tiphys.logging import setup_logging
 23from tiphys.server.api import router
 24from tiphys.server.lock import GatewayLock, LockAcquisitionError, acquire_gateway_lock
 25
 26if TYPE_CHECKING:
 27    pass
 28
 29# Static files directory
 30STATIC_DIR = Path(__file__).parent / "static"
 31
 32logger = structlog.get_logger(__name__)
 33
 34# Global gateway lock (held for lifetime of process)
 35_gateway_lock: GatewayLock | None = None
 36
 37# Startup time for uptime tracking
 38_startup_time: datetime | None = None
 39
 40
 41@asynccontextmanager
 42async def lifespan(app: FastAPI) -> AsyncIterator[None]:
 43    """Application lifespan handler for startup and shutdown."""
 44    global _gateway_lock, _startup_time
 45
 46    config = load_config()
 47
 48    # Setup logging
 49    setup_logging(level=config.log_level, json_output=config.log_json)
 50
 51    # Ensure directories exist
 52    config.ensure_dirs()
 53
 54    # Acquire gateway lock (single instance enforcement)
 55    try:
 56        _gateway_lock = acquire_gateway_lock(port=config.port)
 57    except LockAcquisitionError as e:
 58        await logger.aerror(
 59            "Failed to acquire gateway lock",
 60            error=str(e),
 61            existing_pid=e.existing_lock.pid if e.existing_lock else None,
 62            existing_port=e.existing_lock.port if e.existing_lock else None,
 63        )
 64        # Re-raise to prevent startup
 65        raise
 66
 67    # Create and initialize application context (Composition Root)
 68    ctx = await create_app_context(config=config)
 69
 70    # Store context in app state for dependency injection
 71    app.state.context = ctx
 72
 73    _startup_time = datetime.utcnow()
 74
 75    await logger.ainfo(
 76        "Tiphys starting",
 77        version="0.1.0",
 78        host=config.host,
 79        port=config.port,
 80        model=config.model,
 81        agent_id=config.agent_id,
 82        pid=_gateway_lock.lock_info.pid if _gateway_lock.lock_info else None,
 83    )
 84
 85    try:
 86        yield
 87    finally:
 88        # Shutdown application context
 89        await ctx.shutdown()
 90
 91        # Release gateway lock
 92        if _gateway_lock:
 93            _gateway_lock.release()
 94            _gateway_lock = None
 95
 96        await logger.ainfo("Tiphys shutting down")
 97
 98
 99def create_app(config: TiphysConfig | None = None) -> FastAPI:
100    """
101    Create and configure the FastAPI application.
102
103    Args:
104        config: Optional configuration. If not provided, loads from environment.
105
106    Returns:
107        Configured FastAPI application.
108    """
109    if config is None:
110        config = load_config()
111
112    app = FastAPI(
113        title="Tiphys",
114        description="Local-first, extensible AI agent framework",
115        version="0.1.0",
116        lifespan=lifespan,
117    )
118
119    # CORS middleware
120    app.add_middleware(
121        CORSMiddleware,
122        allow_origins=config.cors_origins,
123        allow_credentials=True,
124        allow_methods=["*"],
125        allow_headers=["*"],
126    )
127
128    # Include API routes
129    app.include_router(router)
130
131    # Serve static files
132    if STATIC_DIR.exists():
133        app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
134
135        @app.get("/")
136        async def serve_index() -> FileResponse:
137            """Serve the main HTML page."""
138            return FileResponse(STATIC_DIR / "index.html")
139
140    return app
141
142
143# Create the default app instance
144app = create_app()
145
146
147def cli() -> None:
148    """CLI entry point (delegates to new click CLI)."""
149    from tiphys.cli.main import cli as click_cli
150
151    click_cli(obj={})
152
153
154def get_uptime_seconds() -> float:
155    """Get server uptime in seconds."""
156    if _startup_time is None:
157        return 0.0
158    return (datetime.utcnow() - _startup_time).total_seconds()
159
160
161def get_startup_time() -> datetime | None:
162    """Get server startup time."""
163    return _startup_time
164
165
166def get_app_context() -> AppContext:
167    """
168    FastAPI dependency to get the current AppContext.
169
170    Usage in routes:
171        @app.get("/example")
172        async def example(ctx: AppContext = Depends(get_app_context)):
173            return {"agent_id": ctx.config.agent_id}
174    """
175    return get_context()
176
177
178if __name__ == "__main__":
179    cli()
STATIC_DIR = PosixPath('/Users/bytedance/projects/tiphys/src/tiphys/static')
logger = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=('tiphys.main',))>
@asynccontextmanager
async def lifespan(app: fastapi.applications.FastAPI) -> AsyncIterator[None]:
42@asynccontextmanager
43async def lifespan(app: FastAPI) -> AsyncIterator[None]:
44    """Application lifespan handler for startup and shutdown."""
45    global _gateway_lock, _startup_time
46
47    config = load_config()
48
49    # Setup logging
50    setup_logging(level=config.log_level, json_output=config.log_json)
51
52    # Ensure directories exist
53    config.ensure_dirs()
54
55    # Acquire gateway lock (single instance enforcement)
56    try:
57        _gateway_lock = acquire_gateway_lock(port=config.port)
58    except LockAcquisitionError as e:
59        await logger.aerror(
60            "Failed to acquire gateway lock",
61            error=str(e),
62            existing_pid=e.existing_lock.pid if e.existing_lock else None,
63            existing_port=e.existing_lock.port if e.existing_lock else None,
64        )
65        # Re-raise to prevent startup
66        raise
67
68    # Create and initialize application context (Composition Root)
69    ctx = await create_app_context(config=config)
70
71    # Store context in app state for dependency injection
72    app.state.context = ctx
73
74    _startup_time = datetime.utcnow()
75
76    await logger.ainfo(
77        "Tiphys starting",
78        version="0.1.0",
79        host=config.host,
80        port=config.port,
81        model=config.model,
82        agent_id=config.agent_id,
83        pid=_gateway_lock.lock_info.pid if _gateway_lock.lock_info else None,
84    )
85
86    try:
87        yield
88    finally:
89        # Shutdown application context
90        await ctx.shutdown()
91
92        # Release gateway lock
93        if _gateway_lock:
94            _gateway_lock.release()
95            _gateway_lock = None
96
97        await logger.ainfo("Tiphys shutting down")

Application lifespan handler for startup and shutdown.

def create_app( config: tiphys.TiphysConfig | None = None) -> fastapi.applications.FastAPI:
100def create_app(config: TiphysConfig | None = None) -> FastAPI:
101    """
102    Create and configure the FastAPI application.
103
104    Args:
105        config: Optional configuration. If not provided, loads from environment.
106
107    Returns:
108        Configured FastAPI application.
109    """
110    if config is None:
111        config = load_config()
112
113    app = FastAPI(
114        title="Tiphys",
115        description="Local-first, extensible AI agent framework",
116        version="0.1.0",
117        lifespan=lifespan,
118    )
119
120    # CORS middleware
121    app.add_middleware(
122        CORSMiddleware,
123        allow_origins=config.cors_origins,
124        allow_credentials=True,
125        allow_methods=["*"],
126        allow_headers=["*"],
127    )
128
129    # Include API routes
130    app.include_router(router)
131
132    # Serve static files
133    if STATIC_DIR.exists():
134        app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
135
136        @app.get("/")
137        async def serve_index() -> FileResponse:
138            """Serve the main HTML page."""
139            return FileResponse(STATIC_DIR / "index.html")
140
141    return app

Create and configure the FastAPI application.

Args: config: Optional configuration. If not provided, loads from environment.

Returns: Configured FastAPI application.

app = <fastapi.applications.FastAPI object>
def cli() -> None:
148def cli() -> None:
149    """CLI entry point (delegates to new click CLI)."""
150    from tiphys.cli.main import cli as click_cli
151
152    click_cli(obj={})

CLI entry point (delegates to new click CLI).

def get_uptime_seconds() -> float:
155def get_uptime_seconds() -> float:
156    """Get server uptime in seconds."""
157    if _startup_time is None:
158        return 0.0
159    return (datetime.utcnow() - _startup_time).total_seconds()

Get server uptime in seconds.

def get_startup_time() -> datetime.datetime | None:
162def get_startup_time() -> datetime | None:
163    """Get server startup time."""
164    return _startup_time

Get server startup time.

def get_app_context() -> tiphys.context.AppContext:
167def get_app_context() -> AppContext:
168    """
169    FastAPI dependency to get the current AppContext.
170
171    Usage in routes:
172        @app.get("/example")
173        async def example(ctx: AppContext = Depends(get_app_context)):
174            return {"agent_id": ctx.config.agent_id}
175    """
176    return get_context()

FastAPI dependency to get the current AppContext.

Usage in routes: @app.get("/example") async def example(ctx: AppContext = Depends(get_app_context)): return {"agent_id": ctx.config.agent_id}