Coverage for agentos/models/response.py: 89%
63 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
1"""AgentOS Response Models — RFC 9457 compatible, OpenAPI-ready.
3Provides the canonical API response envelope used across all endpoints.
4All responses are wrapped in APIResponse[T] for consistency.
5"""
7from __future__ import annotations
9from datetime import UTC, datetime
10from typing import Generic, TypeVar
12from pydantic import BaseModel, Field, field_validator, model_validator
14T = TypeVar("T")
17# ============================================================================
18# Meta & pagination
19# ============================================================================
22class APIResponseMeta(BaseModel):
23 """Response metadata: timing, version, request tracking."""
25 timestamp: str = Field(
26 default_factory=lambda: datetime.now(UTC).isoformat(),
27 description="ISO 8601 response timestamp",
28 )
29 version: str = Field(default="1.0", description="API version")
30 request_id: str = Field(default="", description="Unique request ID for tracing")
33class PaginationMeta(BaseModel):
34 """Cursor-based pagination metadata."""
36 page: int = Field(default=1, ge=1, description="Current page (1-based)")
37 page_size: int = Field(default=20, ge=1, le=200, description="Items per page")
38 total_items: int = Field(default=0, ge=0, description="Total matching items")
39 total_pages: int = Field(default=0, ge=0, description="Total pages")
40 next_cursor: str | None = Field(default=None, description="Opaque cursor for next page")
41 has_next: bool = Field(default=False, description="Whether a next page exists")
42 has_prev: bool = Field(default=False, description="Whether a previous page exists")
44 @field_validator("total_pages", mode="before")
45 @classmethod
46 def compute_total_pages(cls, v, info):
47 if v == 0 and info.data.get("total_items", 0) > 0:
48 page_size = info.data.get("page_size", 20)
49 return max(1, (info.data["total_items"] + page_size - 1) // page_size)
50 return v
52 @model_validator(mode="after")
53 def compute_pagination_flags(self):
54 self.has_next = self.page < self.total_pages
55 self.has_prev = self.page > 1
56 return self
59# ============================================================================
60# Error detail (RFC 9457 Problem Details)
61# ============================================================================
64class APIErrorDetail(BaseModel):
65 """Single error entry conforming to RFC 9457 Problem Details."""
67 type: str = Field(
68 default="about:blank",
69 description="URI reference identifying the problem type",
70 )
71 title: str = Field(description="Short, human-readable summary")
72 status: int = Field(default=500, ge=100, le=599)
73 detail: str = Field(default="", description="Human-readable explanation")
74 instance: str | None = Field(
75 default=None, description="URI reference identifying the specific occurrence"
76 )
77 code: str = Field(default="INTERNAL_ERROR", description="Machine-readable error code")
78 field: str | None = Field(default=None, description="Field name for validation errors")
81# ============================================================================
82# Response envelope
83# ============================================================================
86class APIResponse(BaseModel, Generic[T]):
87 """Canonical API response envelope.
89 All endpoints return this structure with the generic type T for the data field.
91 Success:
92 {"success": true, "data": {...}, "meta": {...}}
94 Error:
95 {"success": false, "error": {...}, "meta": {...}}
96 """
98 success: bool = Field(default=True, description="Whether the request succeeded")
99 data: T | None = Field(default=None, description="Response payload")
100 error: APIErrorDetail | None = Field(
101 default=None, description="Error detail (only when success=False)"
102 )
103 meta: APIResponseMeta = Field(default_factory=APIResponseMeta, description="Response metadata")
106class PaginatedResponse(BaseModel, Generic[T]):
107 """Paginated list response."""
109 success: bool = Field(default=True)
110 data: list[T] = Field(default_factory=list, description="Page items")
111 pagination: PaginationMeta = Field(
112 default_factory=PaginationMeta, description="Pagination metadata"
113 )
114 meta: APIResponseMeta = Field(default_factory=APIResponseMeta, description="Response metadata")
117# ============================================================================
118# Health & version
119# ============================================================================
122class HealthComponent(BaseModel):
123 """Individual component health status."""
125 name: str
126 status: str = Field(description="healthy | degraded | unhealthy")
127 latency_ms: float = Field(default=0.0, description="Check latency in ms")
128 error: str | None = Field(default=None)
131class HealthResponse(BaseModel):
132 """Full health check response."""
134 status: str = Field(description="healthy | degraded | unhealthy")
135 uptime_seconds: float
136 components: list[HealthComponent] = Field(default_factory=list)
137 timestamp: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
140class VersionResponse(BaseModel):
141 """Version info response."""
143 version: str
144 build: str = ""
145 commit_sha: str | None = None
146 python_version: str = ""
147 environment: str = "production"