feat(T07): KI-Copilot with OpenRouter chat, voice input, action system, chat history

This commit is contained in:
2026-07-17 02:28:41 +02:00
parent cb89e3ff1e
commit 5cc9f8e9a9
24 changed files with 2878 additions and 188 deletions
+123
View File
@@ -0,0 +1,123 @@
"""SQLAlchemy models for Copilot chat sessions and messages.
Stores per-user chat history with action proposals from the AI assistant.
"""
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class CopilotRole(str, enum.Enum):
"""Roles for chat messages."""
user = "user"
assistant = "assistant"
class CopilotSession(Base):
"""A chat session belonging to a user. Groups related messages."""
__tablename__ = "copilot_sessions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
title: Mapped[str] = mapped_column(
String(255), nullable=False, default="Neue Konversation",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
messages: Mapped[list["CopilotChat"]] = relationship(
back_populates="session",
cascade="all, delete-orphan",
order_by="CopilotChat.created_at.asc()",
lazy="selectin",
)
def __repr__(self) -> str:
return f"<CopilotSession id={self.id} user_id={self.user_id} title={self.title}>"
def to_dict(self) -> dict:
"""Serialize session for API responses."""
return {
"id": str(self.id),
"user_id": str(self.user_id),
"title": self.title,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"messages": [m.to_dict() for m in (self.messages or [])],
}
class CopilotChat(Base):
"""A single chat message (user or assistant) within a session."""
__tablename__ = "copilot_chats"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("copilot_sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
role: Mapped[CopilotRole] = mapped_column(
Enum(CopilotRole, name="copilot_role"),
nullable=False,
)
content: Mapped[str] = mapped_column(Text, nullable=False)
actions: Mapped[list | None] = mapped_column(
JSONB, nullable=True, default=None,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(),
)
session: Mapped["CopilotSession"] = relationship(back_populates="messages")
def __repr__(self) -> str:
return f"<CopilotChat id={self.id} role={self.role} session_id={self.session_id}>"
def to_dict(self) -> dict:
"""Serialize chat message for API responses."""
return {
"id": str(self.id),
"session_id": str(self.session_id),
"user_id": str(self.user_id),
"role": self.role.value if isinstance(self.role, CopilotRole) else self.role,
"content": self.content,
"actions": self.actions,
"created_at": self.created_at.isoformat() if self.created_at else None,
}