diff --git a/CLAUDE.md b/CLAUDE.md index 81f2048..72c1bf0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,11 +57,11 @@ Create `.env` file with: ### Data Layer **models.py**: Pydantic models for: -- `AiAgent`: Core agent model with comprehensive fields +- `AiAgent`: Core agent model with comprehensive fields (includes `discipline`, `rating`) - `UsageTimelineEntry`: Daily usage data including `message_count` and `token_count` - `UserCreate/UserResponse`: User management models -- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`) -- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`) +- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`, `discipline`, `rating`) +- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`, `discipline`) - `AgentUsageStatsResponse`: Usage statistics response (includes `total_tokens`) **crud.py**: Database operations using Motor (async MongoDB driver): @@ -118,9 +118,20 @@ Located in `templates/` directory: - Status tracking (Active, Inactive, Development, Deprecated) - Rich metadata: tags, userbase, department, contact person - Search functionality across multiple fields -- Filtering by status and audit status (Audited / Not Audited) +- Filtering by status, audit status (Audited / Not Audited), and discipline - Admin can view/manage all agents +### Discipline & Star Rating +- `discipline` field classifies agents into business categories: Strategy, Creative, Oversight including delivery, Optimization, Back Office including operations +- Required on registration form, optional on edit (to support legacy agents) +- `rating` field is a 1-5 star rating for quality assessment by discipline leads +- Interactive star rating widget in detail modal saves immediately via `PUT /api/agents/{id}/rating` +- Star rating also editable in the edit modal (both agent management page and admin dashboard) +- Dashboard supports filtering by discipline and sorting by rating +- Discipline badge (purple) and star rating badge displayed on agent cards +- Both fields included in CSV export/import +- Discipline passed through collector API; rating is human-only (not in collector) + ### Token Usage Tracking - `total_tokens` field on agents tracks cumulative LLM token consumption - `token_count` per day in usage timeline entries alongside `message_count` diff --git a/crud.py b/crud.py index 127a69d..898b1d8 100644 --- a/crud.py +++ b/crud.py @@ -151,6 +151,12 @@ async def create_agent(agent_data: dict, user_id: str, skip_time_check: bool = F agent_doc["quality_audit_updated_by_name"] = None if "risk_factor" not in agent_doc: agent_doc["risk_factor"] = None + if "discipline" not in agent_doc: + agent_doc["discipline"] = None + if "rating" not in agent_doc: + agent_doc["rating"] = None + if "total_tokens" not in agent_doc: + agent_doc["total_tokens"] = None result = await agents_collection.insert_one(agent_doc) agent_doc["_id"] = result.inserted_id return agent_doc @@ -198,7 +204,8 @@ async def search_agents(search_term: str, user_id: str = None): {"agent_name": {"$regex": search_term, "$options": "i"}}, {"agent_description": {"$regex": search_term, "$options": "i"}}, {"agent_tags": {"$regex": search_term, "$options": "i"}}, - {"agent_department": {"$regex": search_term, "$options": "i"}} + {"agent_department": {"$regex": search_term, "$options": "i"}}, + {"discipline": {"$regex": search_term, "$options": "i"}} ] } @@ -403,7 +410,7 @@ def _agent_data_differs(existing_agent: dict, new_agent_data: dict) -> bool: "agent_description", "agent_purpose", "agent_version", "agent_status", "agent_location", "agent_department", "agent_contact_person", "agent_tags", "agent_userbase", "agent_capabilities", "agent_metadata", - "url" + "url", "discipline" ] for field in comparable_fields: diff --git a/main.py b/main.py index d01aea6..f9e4ad6 100644 --- a/main.py +++ b/main.py @@ -225,6 +225,8 @@ def create_agent_response(agent: dict) -> models.AiAgentResponse: quality_audit_updated_by_name=agent.get("quality_audit_updated_by_name"), risk_factor=agent.get("risk_factor"), last_edited_by=agent.get("last_edited_by"), + discipline=agent.get("discipline"), + rating=agent.get("rating"), created_by=agent["created_by"], # Usage tracking fields usage_timeline=agent.get("usage_timeline"), @@ -279,6 +281,7 @@ def map_agent_collector_to_internal(collector_data: models.AgentCollectorCreate) "agent_tags": collector_data.tags, "agent_metadata": collector_data.metadata, "url": collector_data.url, + "discipline": collector_data.discipline, # Usage tracking fields "usage_timeline": usage_timeline, "conversation_count": collector_data.conversation_count, @@ -570,6 +573,7 @@ async def agent_register_form( agent_location: str = Form(None), agent_department: str = Form(None), agent_contact_person: str = Form(None), + discipline: str = Form(...), agent_tags: str = Form(None), agent_userbase: str = Form(None), agent_capabilities: str = Form(None), @@ -595,6 +599,7 @@ async def agent_register_form( "agent_location": agent_location, "agent_department": agent_department, "agent_contact_person": agent_contact_person, + "discipline": discipline, } # Process tags, userbase, and capabilities (convert comma-separated to lists) @@ -886,6 +891,28 @@ async def update_agent(agent_id: str, agent: models.AiAgentCreate, current_user: return create_agent_response(updated_agent) +@app.put("/api/agents/{agent_id}/rating") +async def update_agent_rating(agent_id: str, request: Request, current_user: dict = Depends(get_current_user_from_cookie)): + """Update only the star rating of an agent (lightweight endpoint for immediate saves)""" + existing_agent = await crud.get_agent_by_id(agent_id) + if not existing_agent: + raise HTTPException(status_code=404, detail="Agent not found") + + if not can_user_edit_agent(existing_agent, current_user): + raise HTTPException(status_code=403, detail="Not authorized to rate this agent") + + body = await request.json() + rating_value = body.get("rating") + + if rating_value is None or not (1 <= float(rating_value) <= 5): + raise HTTPException(status_code=422, detail="Rating must be between 1 and 5") + + updated = await crud.update_agent(agent_id, {"rating": float(rating_value)}, last_edited_by=current_user.get("email")) + if not updated: + raise HTTPException(status_code=500, detail="Failed to update rating") + + return {"message": "Rating updated", "rating": float(rating_value)} + @app.delete("/api/agents/{agent_id}") async def delete_agent(agent_id: str, current_user: dict = Depends(get_current_user_from_cookie)): print(f"🗑️ DELETE attempt - Agent ID: {agent_id}, User ID: {current_user['_id']}") @@ -1086,7 +1113,9 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)): "risk_factor", "last_edited_by", "created_by", - "total_tokens" + "total_tokens", + "discipline", + "rating" ] writer = csv.DictWriter(output, fieldnames=fieldnames) @@ -1121,7 +1150,9 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)): "risk_factor": str(agent.get("risk_factor", "")) if agent.get("risk_factor") is not None else "", "last_edited_by": agent.get("last_edited_by", ""), "created_by": agent.get("created_by", ""), - "total_tokens": str(agent.get("total_tokens", "")) if agent.get("total_tokens") is not None else "" + "total_tokens": str(agent.get("total_tokens", "")) if agent.get("total_tokens") is not None else "", + "discipline": agent.get("discipline", ""), + "rating": str(agent.get("rating", "")) if agent.get("rating") is not None else "" } writer.writerow(row) @@ -1180,7 +1211,10 @@ async def import_agents_csv( "agent_contact_person": row.get("agent_contact_person"), "url": row.get("url"), "quality_audit_status": row.get("quality_audit_status", "False").lower() == "true", - "risk_factor": int(row.get("risk_factor")) if row.get("risk_factor") else None + "risk_factor": int(row.get("risk_factor")) if row.get("risk_factor") else None, + "discipline": row.get("discipline") or None, + "rating": float(row.get("rating")) if row.get("rating") else None, + "total_tokens": int(row.get("total_tokens")) if row.get("total_tokens") else None } # Handle lists (pipe separated) @@ -1323,6 +1357,7 @@ async def create_agent_collector( # Update agent document with new usage data (replace strategy) update_fields = { "url": internal_data.get("url"), + "discipline": internal_data.get("discipline"), "usage_timeline": internal_data.get("usage_timeline"), "conversation_count": internal_data.get("conversation_count"), "unique_users": internal_data.get("unique_users"), diff --git a/models.py b/models.py index 0ccb554..9064bf0 100644 --- a/models.py +++ b/models.py @@ -32,6 +32,8 @@ class AiAgent(BaseModel): 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) @@ -98,6 +100,8 @@ class AiAgentCreate(BaseModel): 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) class AiAgentResponse(BaseModel): agent_id: str @@ -123,6 +127,8 @@ class AiAgentResponse(BaseModel): 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 created_by: str # Usage tracking fields (new) @@ -152,6 +158,7 @@ class AgentCollectorCreate(BaseModel): tags: Optional[list[str]] = None metadata: Optional[dict] = None url: Optional[str] = None + discipline: Optional[str] = None # Usage tracking fields (new) usage_timeline: Optional[List[UsageTimelineEntry]] = None diff --git a/static/style.css b/static/style.css index b5c89b4..7cb1d29 100644 --- a/static/style.css +++ b/static/style.css @@ -402,4 +402,25 @@ h2 { .container h2 { -webkit-text-fill-color: var(--text-dark); color: var(--text-dark); +} + +/* Star rating styles */ +.star-interactive, .star-clickable { + cursor: pointer; + font-size: 1.3rem; + transition: color 0.2s, transform 0.2s; + color: #dee2e6; +} + +.star-interactive:hover, .star-clickable:hover { + transform: scale(1.2); +} + +.star-rating-edit { + padding: 8px 0; +} + +/* Discipline badge */ +.bg-purple { + background-color: #6f42c1 !important; } \ No newline at end of file diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index db2d8ed..9524cd1 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -416,7 +416,36 @@ - + +