- Add vector store with sentence-transformers for semantic search - FastAPI backend with /chat and /health endpoints - Conversation state persistence via SQLite - OpenRouter integration with structured JSON responses - Discord bot with /ask slash command and reply-based follow-ups - Automated Gitea issue creation for unanswered questions - Docker support with docker-compose for easy deployment - Example rule file and ingestion script - Comprehensive documentation in README
101 lines
2.9 KiB
Python
101 lines
2.9 KiB
Python
"""Data models for rules and conversations."""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
|
|
|
|
class RuleMetadata(BaseModel):
|
|
"""Frontmatter metadata for a rule document."""
|
|
|
|
rule_id: str = Field(..., description="Unique rule identifier, e.g. '5.2.1(b)'")
|
|
title: str = Field(..., description="Rule title")
|
|
section: str = Field(..., description="Section/category name")
|
|
parent_rule: Optional[str] = Field(
|
|
None, description="Parent rule ID for hierarchical rules"
|
|
)
|
|
last_updated: str = Field(
|
|
default_factory=lambda: datetime.now().strftime("%Y-%m-%d"),
|
|
description="Last update date",
|
|
)
|
|
page_ref: Optional[str] = Field(
|
|
None, description="Reference to page number in rulebook"
|
|
)
|
|
|
|
|
|
class RuleDocument(BaseModel):
|
|
"""Complete rule document with metadata and content."""
|
|
|
|
metadata: RuleMetadata
|
|
content: str = Field(..., description="Rule text and examples")
|
|
source_file: str = Field(..., description="Source file path")
|
|
embedding: Optional[list[float]] = None
|
|
|
|
def to_chroma_metadata(self) -> dict:
|
|
"""Convert to ChromaDB metadata format."""
|
|
return {
|
|
"rule_id": self.metadata.rule_id,
|
|
"title": self.metadata.title,
|
|
"section": self.metadata.section,
|
|
"parent_rule": self.metadata.parent_rule or "",
|
|
"page_ref": self.metadata.page_ref or "",
|
|
"last_updated": self.metadata.last_updated,
|
|
"source_file": self.source_file,
|
|
}
|
|
|
|
|
|
class Conversation(BaseModel):
|
|
"""Conversation session."""
|
|
|
|
id: str
|
|
user_id: str # Discord user ID
|
|
channel_id: str # Discord channel ID
|
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
last_activity: datetime = Field(default_factory=datetime.now)
|
|
|
|
|
|
class Message(BaseModel):
|
|
"""Individual message in a conversation."""
|
|
|
|
id: str
|
|
conversation_id: str
|
|
content: str
|
|
is_user: bool
|
|
parent_id: Optional[str] = None
|
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
"""Incoming chat request from Discord."""
|
|
|
|
message: str
|
|
conversation_id: Optional[str] = None
|
|
parent_message_id: Optional[str] = None
|
|
user_id: str
|
|
channel_id: str
|
|
|
|
|
|
class ChatResponse(BaseModel):
|
|
"""Response to chat request."""
|
|
|
|
response: str
|
|
conversation_id: str
|
|
message_id: str
|
|
parent_message_id: Optional[str] = None
|
|
cited_rules: list[str] = Field(default_factory=list)
|
|
confidence: float = Field(..., ge=0.0, le=1.0)
|
|
needs_human: bool = Field(
|
|
default=False,
|
|
description="Whether the question needs human review (unanswered)",
|
|
)
|
|
|
|
|
|
class RuleSearchResult(BaseModel):
|
|
"""Result from vector search."""
|
|
|
|
rule_id: str
|
|
title: str
|
|
content: str
|
|
section: str
|
|
similarity: float = Field(..., ge=0.0, le=1.0)
|