Files
ai-agent/backend/schemas/projects.py
Gabriel Vidal 2119af66d5 refactor(schemas): split backend/schemas.py into a schemas/ package
One module per API area (base, files, activity, skills, conversations,
notifications, forms, search, plans, projects, services, misc, diff, cron,
agents) on a shared base.Schema; __init__.py re-exports every model so
handlers keep writing schemas.<Name>. OpenAPI output is byte-identical.
PlanSummary lives in conversations.py to keep the module graph a DAG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:55:47 +02:00

263 lines
7.3 KiB
Python

"""Projects, their GOAL.md (+ the goals board at ``/api/goals``) and the
scaffold templates."""
from __future__ import annotations
from typing import Literal, Optional
from .base import AccountSplit, Schema
__all__ = [
"ProjectCostSummary",
"TokenTypeSpend",
"ProjectByType",
"ProjectCostDetail",
"GoalSummary",
"GoalChecklistItem",
"GoalClaim",
"GoalDetail",
"GoalMilestone",
"GoalTheme",
"RunningConversation",
"GoalBoardEntry",
"GoalsResponse",
"ProjectSummary",
"IndexMeta",
"OgImage",
"KeyFile",
"ProjectCommit",
"ProjectDetail",
"ProjectsResponse",
"ProjectEnvVar",
"ProjectEnvResponse",
"TemplateSummary",
"TemplateTreeNode",
"TemplateDetail",
"TemplatesResponse",
]
# ── projects ──────────────────────────────────────────────────────────────────
class ProjectCostSummary(Schema):
cost: float
tokens: int
conversations: int
# The Claude account owning the project (orus-monorepo → "work") and
# whether that account is left out of aggregated totals. For such a project
# the figures above are that account's spend even in the default view.
account: Optional[str] = None
excludeFromTotals: Optional[bool] = None
byAccount: Optional[dict[str, AccountSplit]] = None
class TokenTypeSpend(Schema):
tokens: int
cost: float
class ProjectByType(Schema):
input: TokenTypeSpend
output: TokenTypeSpend
cacheRead: TokenTypeSpend
cacheWrite: TokenTypeSpend
class ProjectCostDetail(ProjectCostSummary):
meanCost: float
medianCost: float
meanTokens: float
medianTokens: float
byType: ProjectByType
loc: int
meanCostPerLoc: float
# A directory's GOAL.md: the checklist rollup that tags a card, and — on a
# detail response — the markdown itself. Absent file ⇒ the field is None.
class GoalSummary(Schema):
done: int
total: int
# One checklist item on a detail page's Goal checklist widget: the raw markdown
# after its checkbox (rendered), a plain single-line summary (the Work prompt +
# tooltip), and whether it's ticked.
class GoalChecklistItem(Schema):
text: str
plain: str
checked: bool
# A goal-keeper's in-flight claim under "## Being worked on" — an item some agent
# is on right now. Surfaced on the detail (so the checklist can flag a claimed
# item) and on the board. (Defined here so GoalDetail can reference it.)
class GoalClaim(Schema):
text: str
since: Optional[str] = None # ISO — parsed off a trailing "@<iso>"
stale: bool # older than the goal-keeper's 5h cadence
class GoalDetail(GoalSummary):
content: str
items: list[GoalChecklistItem]
claims: list[GoalClaim]
# ── the goals board (/api/goals) ─────────────────────────────────────────────
# One card per project/service that keeps a GOAL.md: its progress, the version
# milestones off its "## Horizons", the themed wishlist groups, the goal-keeper
# claims in "## Being worked on", and how many agents are on it right now.
class GoalMilestone(Schema):
title: str
version: Optional[str] = None # "0.3" — absent on an unversioned horizon
current: bool # the horizon marked "(now)"
done: int
total: int
hasTasks: bool # False ⇒ prose horizon, don't render "0/0"
class GoalTheme(Schema):
title: str
done: int
total: int
class RunningConversation(Schema):
id: str
title: str
startedAt: Optional[str] = None
class GoalBoardEntry(GoalSummary):
kind: Literal["project", "service"]
dir: str
name: str
description: str
url: Optional[str] = None
updatedAt: Optional[str] = None
milestones: list[GoalMilestone]
themes: list[GoalTheme]
claims: list[GoalClaim]
# The goal's checklist, so a card can expand into its individual items (and
# put a Work button on each) without a round-trip to the detail page. Capped
# server-side — see `goal.BOARD_ITEMS` — since the board renders many goals.
items: list[GoalChecklistItem]
itemsTruncated: int # items beyond the cap, for a "+N more" hint
agents: int # sessions running on this unit right now
runningConversations: list[RunningConversation]
conversations: int
lastConversationAt: Optional[str] = None
class GoalsResponse(Schema):
goals: list[GoalBoardEntry]
class ProjectSummary(Schema):
dir: str
name: str
description: str
version: Optional[str] = None
url: Optional[str] = None
stack: list[str]
keywords: list[str]
hasPackageJson: bool
goal: Optional[GoalSummary] = None
updatedAt: Optional[str] = None
costs: Optional[ProjectCostSummary] = None
# True for the root project — the homelab repo itself, which collects every
# conversation that didn't work on a project of its own. Rendered distinctly
# (it isn't a `projects/<slug>` directory like the others).
isRoot: Optional[bool] = None
# The Claude account whose sessions work on this project (accounts.py).
account: Optional[str] = None
class IndexMeta(Schema):
title: Optional[str] = None
description: Optional[str] = None
ogImage: Optional[str] = None
ogUrl: Optional[str] = None
themeColor: Optional[str] = None
class OgImage(Schema):
kind: Literal["url", "asset"]
src: str
class KeyFile(Schema):
name: str
bytes: int
class ProjectCommit(Schema):
hash: str
date: str
subject: str
class ProjectDetail(ProjectSummary):
readme: Optional[str] = None
claudeMd: Optional[str] = None
goal: Optional[GoalDetail] = None
indexMeta: Optional[IndexMeta] = None
ogImage: Optional[OgImage] = None
scripts: list[str]
keyFiles: list[KeyFile]
commits: list[ProjectCommit]
costs: Optional[ProjectCostDetail] = None
class ProjectsResponse(Schema):
projects: list[ProjectSummary]
class ProjectEnvVar(Schema):
key: str
# Secrets never travel to the UI: value is None for them, hasValue says
# whether a non-empty value is set on disk.
value: Optional[str] = None
secret: bool
hasValue: bool
class ProjectEnvResponse(Schema):
slug: str
exists: bool
vars: list[ProjectEnvVar]
# ── templates ─────────────────────────────────────────────────────────────────
class TemplateSummary(Schema):
name: str
description: str
stack: list[str]
fileCount: int
sizeBytes: int
hasReadme: bool
class TemplateTreeNode(Schema):
name: str
path: str
type: Literal["dir", "file"]
size: Optional[int] = None
text: Optional[bool] = None
children: Optional[list["TemplateTreeNode"]] = None
class TemplateDetail(Schema):
name: str
description: str
stack: list[str]
fileCount: int
sizeBytes: int
readme: Optional[str] = None
scripts: dict[str, str]
dependencies: list[str]
tree: list[TemplateTreeNode]
class TemplatesResponse(Schema):
templates: list[TemplateSummary]