agent_tracker/models.py
nickviljoen 54ecd31bdd Add registration form redesign with completion flow for LibreChat-synced agents
Rebuilds the agent registration form into 7 governance sections (Identity,
Classification, Autonomy, IP, Tech Stack, Data Safety, Performance, Declarations)
and introduces a completion flow for agents that come in via the LibreChat
collector without the new required fields.

- New form fields: business_entity, client_scope, agent_classification,
  autonomy_level, ip_ownership, foundation_model, validated_by/date, evals_method,
  plus nested safety / pii / declarations objects and a registration_complete flag.
- registration_complete defaults to true for form-submitted agents and false for
  collector-created ones; existing agents are grandfathered via a startup migration.
- Owner-by-email lookup so LibreChat-synced agents surface in the user's "My Agents"
  view with an Incomplete badge and Complete CTA. Submitting the completion form
  reassigns created_by from the collector marker to the user.
- Daily APScheduler job sends a digest reminder email per owner with a 7-day
  cooldown and 4-nudge cap (configurable). Manual trigger via
  POST /api/admin/completion-reminders/send.
- Admin banner + modal for collector agents whose contact email doesn't match an
  active user, with one-click reassignment.
- Gemini audit extended to also return agent_classification and an autonomy hint;
  applied to agents on next batch run alongside discipline/department.
- New filter dimensions on agent management + admin dashboard: Business Entity,
  Agent Type, Autonomy, plus a Compliance Risks quick toggle.
- CSV export/import gains 21 columns covering all governance fields.
- Discipline 'Optimization' renamed to 'Optimisation' with idempotent startup
  migration; Gemini system prompt and template dropdowns updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:26:55 +02:00

330 lines
14 KiB
Python

from pydantic import BaseModel, EmailStr, Field
from typing import Optional, List
class UsageTimelineEntry(BaseModel):
date: str = Field(..., description="Date in YYYY-MM-DD format")
message_count: int = Field(..., ge=0, description="Number of messages on this date")
token_count: int = Field(0, ge=0, description="Number of tokens consumed on this date")
# Closed-list values for the new governance/registration fields. Kept as module-level
# constants so the form, API, and migrations all reference the same source of truth.
BUSINESS_ENTITIES = ["OLIVER", "DARE", "Brandtech Group", "Pencil", "Jellyfish", "Adjust", "Other"]
CLIENT_SCOPES = ["internal", "all", "specific"]
AGENT_CLASSIFICATIONS = ["Utility", "Functional", "Supervisory", "Guardian"]
AUTONOMY_LEVELS = ["Human-Led", "Hybrid", "Autopilot"]
IP_OWNERSHIPS = ["Brandtech IP", "Client IP", "Shared/TBD"]
DISCIPLINES = [
"Strategy",
"Creative",
"Oversight including delivery",
"Optimisation",
"Back Office including operations",
"Pencil Agents",
]
class AgentSafety(BaseModel):
off_switch_confirmed: Optional[bool] = None
access_rights_confirmed: Optional[bool] = None
class AgentPII(BaseModel):
handles_pii: Optional[bool] = None
legal_ref: Optional[str] = None
data_types: Optional[str] = None
consent_recorded: Optional[bool] = None
class AgentDeclarations(BaseModel):
governance: Optional[bool] = None
accuracy: Optional[bool] = None
upkeep: Optional[bool] = None
class AiAgent(BaseModel):
agent_id: int
agent_name: str
agent_tool: str | None = Field(default=None, title="The tool or platform where the agent operates", max_length=100)
agent_description: str | None = Field(default=None, title="The description of the agent", max_length=300)
agent_purpose: str | None = Field(default=None, title="The purpose of the agent", max_length=200)
agent_version: str | None = Field(default=None, title="The version of the agent", max_length=100)
agent_status: str | None = Field(default=None, title="The status of the agent", max_length=100, enum=['Active', 'Inactive', 'Deprecated', 'Development'])
agent_location: str | None = Field(default=None, title="The location of the agent", max_length=100)
agent_department: str | None = Field(default=None, title="The department of the agent", max_length=100)
agent_contact_person: str | None = Field(default=None, title="The contact person for the agent", max_length=100)
agent_created_at: str | None = Field(default=None, title="The creation date of the agent", max_length=100)
agent_updated_at: str | None = Field(default=None, title="The last update date of the agent", max_length=100)
agent_tags: list[str] | None = Field(default=None, title="Tags associated with the agent", max_length=100)
agent_metadata: dict[str, str] | None = Field(default=None, title="Metadata associated with the agent")
agent_userbase: list[str] | None = Field(default=None, title="Userbase associated with the agent")
agent_capabilities: list[str] | None = Field(default=None, title="Capabilities of the agent")
url: str | None = Field(default=None, title="Direct link to create a conversation with this agent")
quality_audit_status: bool | None = Field(default=False, title="Quality audit status")
quality_audit_updated_by: str | None = Field(default=None, title="Admin user ID who updated quality audit")
quality_audit_updated_at: str | None = Field(default=None, title="Quality audit last update timestamp")
quality_audit_updated_by_name: str | None = Field(default=None, title="Admin user name who updated quality audit")
risk_factor: int | None = Field(default=None, title="Risk factor rating (1-5)", ge=1, le=5)
last_edited_by: str | None = Field(default=None, title="Email of user who last edited this agent")
discipline: str | None = Field(default=None, title="Business discipline/category", max_length=100)
rating: float | None = Field(default=None, title="Star rating (1-5)", ge=1, le=5)
instructions: str | None = Field(default=None, title="System prompt / instructions from LibreChat")
# Governance / registration fields (introduced 2026-05)
business_entity: Optional[str] = Field(default=None, title="Group company the agent sits under")
client_scope: Optional[str] = Field(default=None, title="internal / all / specific")
agent_classification: Optional[str] = Field(default=None, title="Utility / Functional / Supervisory / Guardian")
autonomy_level: Optional[str] = Field(default=None, title="Human-Led / Hybrid / Autopilot")
ip_ownership: Optional[str] = Field(default=None, title="Brandtech IP / Client IP / Shared/TBD")
foundation_model: Optional[str] = Field(default=None, title="Primary LLM provider/model")
safety: Optional[AgentSafety] = None
pii: Optional[AgentPII] = None
validated_by: Optional[str] = None
validation_date: Optional[str] = None
evals_method: Optional[str] = None
declarations: Optional[AgentDeclarations] = None
registration_complete: Optional[bool] = Field(default=None, title="Whether all required registration fields are populated")
# User Base Model
class UserCreate(BaseModel):
email: EmailStr
password: str
full_name: Optional[str] = None
class UserLogin(BaseModel):
email: EmailStr
password: str
class UserResponse(BaseModel):
email: EmailStr
full_name: Optional[str] = None
is_active: bool
is_admin: bool
role: Optional[str] = "user"
auth_provider: Optional[str] = "local"
class UserUpdate(BaseModel):
full_name: Optional[str] = None
is_active: Optional[bool] = None
is_admin: Optional[bool] = None
role: Optional[str] = Field(default=None, pattern="^(user|admin|readonly_admin)$")
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
# Admin user management models
class AdminUserCreate(BaseModel):
email: EmailStr
full_name: Optional[str] = None
password: str = Field(..., min_length=8)
is_admin: bool = False
class AdminPasswordReset(BaseModel):
new_password: str = Field(..., min_length=8)
class PasswordChange(BaseModel):
current_password: str
new_password: str = Field(..., min_length=8)
# Agent models for creation and response
class AiAgentCreate(BaseModel):
agent_name: str
agent_tool: str
agent_description: Optional[str] = None
agent_purpose: Optional[str] = None
agent_version: Optional[str] = None
agent_status: Optional[str] = "Development"
agent_location: Optional[str] = None
agent_department: Optional[str] = None
agent_contact_person: Optional[str] = None
agent_tags: Optional[list[str]] = None
agent_metadata: Optional[dict[str, str]] = None
agent_userbase: Optional[list[str]] = None
agent_capabilities: Optional[list[str]] = None
url: Optional[str] = None
quality_audit_status: Optional[bool] = False
quality_audit_updated_by: Optional[str] = None
quality_audit_updated_at: Optional[str] = None
quality_audit_updated_by_name: Optional[str] = None
risk_factor: Optional[int] = Field(default=None, ge=1, le=5)
last_edited_by: Optional[str] = None
discipline: Optional[str] = None
rating: Optional[float] = Field(default=None, ge=1, le=5)
client: Optional[str] = None
client_name: Optional[str] = None
studio_name: Optional[str] = None
instructions: Optional[str] = None
# Governance / registration fields (introduced 2026-05)
business_entity: Optional[str] = None
client_scope: Optional[str] = Field(default=None, pattern="^(internal|all|specific)$")
agent_classification: Optional[str] = Field(default=None, pattern="^(Utility|Functional|Supervisory|Guardian)$")
autonomy_level: Optional[str] = Field(default=None, pattern="^(Human-Led|Hybrid|Autopilot)$")
ip_ownership: Optional[str] = None
foundation_model: Optional[str] = None
safety: Optional[AgentSafety] = None
pii: Optional[AgentPII] = None
validated_by: Optional[str] = None
validation_date: Optional[str] = None
evals_method: Optional[str] = None
declarations: Optional[AgentDeclarations] = None
registration_complete: Optional[bool] = None
class AiAgentResponse(BaseModel):
agent_id: str
agent_name: str
agent_tool: Optional[str] = None
agent_description: Optional[str] = None
agent_purpose: Optional[str] = None
agent_version: Optional[str] = None
agent_status: Optional[str] = None
agent_location: Optional[str] = None
agent_department: Optional[str] = None
agent_contact_person: Optional[str] = None
agent_created_at: Optional[str] = None
agent_updated_at: Optional[str] = None
agent_tags: Optional[list[str]] = None
agent_metadata: Optional[dict[str, str]] = None
agent_userbase: Optional[list[str]] = None
agent_capabilities: Optional[list[str]] = None
url: Optional[str] = None
quality_audit_status: Optional[bool] = None
quality_audit_updated_by: Optional[str] = None
quality_audit_updated_at: Optional[str] = None
quality_audit_updated_by_name: Optional[str] = None
risk_factor: Optional[int] = None
last_edited_by: Optional[str] = None
discipline: Optional[str] = None
rating: Optional[float] = None
rating_count: Optional[int] = None
client: Optional[str] = None
client_name: Optional[str] = None
studio_name: Optional[str] = None
verification_status: Optional[str] = None
verified_by: Optional[str] = None
verified_date: Optional[str] = None
instructions: Optional[str] = None
audit_status: Optional[str] = None
audit_date: Optional[str] = None
audit_category: Optional[str] = None
audit_risk_level: Optional[str] = None
created_by: str
# Usage tracking fields (new)
usage_timeline: Optional[List[dict]] = None
conversation_count: Optional[int] = None
unique_users: Optional[int] = None
total_messages: Optional[int] = None
first_used: Optional[str] = None
last_used: Optional[str] = None
total_tokens: Optional[int] = None
prompt_tokens: Optional[int] = None
completion_tokens: Optional[int] = None
# Governance / registration fields (introduced 2026-05)
business_entity: Optional[str] = None
client_scope: Optional[str] = None
agent_classification: Optional[str] = None
autonomy_level: Optional[str] = None
ip_ownership: Optional[str] = None
foundation_model: Optional[str] = None
safety: Optional[AgentSafety] = None
pii: Optional[AgentPII] = None
validated_by: Optional[str] = None
validation_date: Optional[str] = None
evals_method: Optional[str] = None
declarations: Optional[AgentDeclarations] = None
registration_complete: Optional[bool] = None
# Agent Collector API Models (for compatibility with agent_collector app)
class AgentCollectorCreate(BaseModel):
name: str = Field(min_length=1)
description: str = Field(min_length=1)
purpose: str = Field(min_length=1)
tool: str = Field(min_length=1)
location: Optional[str] = None
userbase: Optional[list[str]] = None
version: Optional[str] = None
creation_date: Optional[str] = None # ISO 8601 datetime string
last_updated: Optional[str] = None # ISO 8601 datetime string
capabilities: Optional[list[str]] = None
status: Optional[str] = Field(default="development", pattern="^(?i)(active|inactive|deprecated|development)$")
department: Optional[str] = None
contact_person: Optional[str] = None
tags: Optional[list[str]] = None
metadata: Optional[dict] = None
url: Optional[str] = None
discipline: Optional[str] = None
client: Optional[str] = None
client_name: Optional[str] = None
studio_name: Optional[str] = None
instructions: Optional[str] = None
# Governance / registration fields — accepted optionally so the collector
# can opt into sending them later without an API change.
business_entity: Optional[str] = None
client_scope: Optional[str] = None
agent_classification: Optional[str] = None
autonomy_level: Optional[str] = None
ip_ownership: Optional[str] = None
foundation_model: Optional[str] = None
safety: Optional[AgentSafety] = None
pii: Optional[AgentPII] = None
validated_by: Optional[str] = None
validation_date: Optional[str] = None
evals_method: Optional[str] = None
declarations: Optional[AgentDeclarations] = None
# Usage tracking fields (new)
usage_timeline: Optional[List[UsageTimelineEntry]] = None
conversation_count: Optional[int] = Field(default=None, ge=0)
unique_users: Optional[int] = Field(default=None, ge=0)
total_messages: Optional[int] = Field(default=None, ge=0)
first_used: Optional[str] = None # ISO 8601 datetime string
last_used: Optional[str] = None # ISO 8601 datetime string
total_tokens: Optional[int] = Field(default=None, ge=0)
prompt_tokens: Optional[int] = Field(default=None, ge=0)
completion_tokens: Optional[int] = Field(default=None, ge=0)
class AgentCollectorResponse(BaseModel):
status: str = "success"
message: str = "Agent data collected successfully"
agent_id: str
class HealthCheckResponse(BaseModel):
status: str
message: str
timestamp: str
database: dict
class AgentUsageTrackingResponse(BaseModel):
status: str = "usage_logged"
message: str = "Agent already exists, usage tracked"
agent_name: str
class AgentUsageRecord(BaseModel):
agent_name: str
agent_data: dict
timestamp: str
usage_count: Optional[int] = None
class AuditReviewRequest(BaseModel):
audit_status: str = Field(..., pattern="^(flagged|reviewed|cleared)$")
reviewer_notes: Optional[str] = None
class AgentUsageStatsResponse(BaseModel):
agent_name: str
total_usage_count: int
first_usage: Optional[str] = None
last_usage: Optional[str] = None
usage_by_period: dict
conversation_count: Optional[int] = None
unique_users: Optional[int] = None
total_tokens: Optional[int] = None
prompt_tokens: Optional[int] = None
completion_tokens: Optional[int] = None