This commit is contained in:
2025-09-14 17:15:48 +00:00
commit 4dee2bb54c
40 changed files with 739 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

101
app/api.py Normal file
View File

@@ -0,0 +1,101 @@
# from fastapi import APIRouter, Depends
# from sqlalchemy.ext.asyncio import AsyncSession
# from .database import async_session
# from . import crud, openrouter_client
# router = APIRouter()
# async def get_db():
# async with async_session() as db:
# yield db
# @router.get("/history/{telegram_id}")
# async def get_history(telegram_id: str, db: AsyncSession = Depends(get_db)):
# user = await crud.get_or_create_user(db, telegram_id)
# history = await crud.get_conversation(db, user, limit=20)
# return [{"role": m.role, "content": m.content} for m in history]
# @router.post("/chat/{telegram_id}")
# async def chat(telegram_id: str, payload: dict, db: AsyncSession = Depends(get_db)):
# user = await crud.get_or_create_user(db, telegram_id)
# user_msg = payload.get("message", "")
# if not user_msg:
# return {"error": "Empty message"}
# await crud.add_message(db, user, "user", user_msg)
# history = await crud.get_conversation(db, user, limit=10)
# messages_payload = [{"role": m.role, "content": m.content} for m in history]
# response = await openrouter_client.generate_response(messages_payload)
# await crud.add_message(db, user, "assistant", response)
# return {"response": response}
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from app import models, schemas
from .database import async_session
router = APIRouter()
# --- Dependency ---
async def get_db() -> AsyncSession:
async with async_session() as db:
yield db
# 📌 Получить профиль пользователя (берём первого для примера)
@router.get("/profile", response_model=schemas.UserOut)
async def get_profile(db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(models.User).options(
selectinload(models.User.chats).selectinload(models.Chat.messages)
)
)
user = result.scalars().first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# 📌 Получить список чатов
@router.get("/chats", response_model=list[schemas.ChatOut])
async def get_chats(db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(models.Chat).options(selectinload(models.Chat.messages))
)
return result.scalars().all()
# 📌 Сообщения конкретного чата
@router.get("/chats/{chat_id}/messages", response_model=list[schemas.MessageOut])
async def get_chat_messages(chat_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(models.Chat)
.options(selectinload(models.Chat.messages))
.filter_by(id=chat_id)
)
chat = result.scalars().first()
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")
return chat.messages
# 📌 Добавить новое сообщение
@router.post("/chats/{chat_id}/messages", response_model=schemas.MessageOut)
async def add_message(chat_id: int, msg: schemas.MessageBase, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(models.Chat).filter_by(id=chat_id)
)
chat = result.scalars().first()
if not chat:
raise HTTPException(status_code=404, detail="Chat not found")
new_msg = models.Message(chat_id=chat_id, role=msg.role, content=msg.content)
db.add(new_msg)
await db.commit()
await db.refresh(new_msg)
return new_msg

29
app/crud.py Normal file
View File

@@ -0,0 +1,29 @@
from sqlalchemy.future import select
from sqlalchemy.ext.asyncio import AsyncSession
from . import models
async def get_or_create_user(db: AsyncSession, telegram_id: str):
result = await db.execute(select(models.User).where(models.User.telegram_id == telegram_id))
user = result.scalar_one_or_none()
if not user:
user = models.User(telegram_id=telegram_id)
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def add_message(db: AsyncSession, user, role, content):
msg = models.Message(user_id=user.id, role=role, content=content)
db.add(msg)
await db.commit()
await db.refresh(msg)
return msg
async def get_conversation(db: AsyncSession, user, limit=10):
result = await db.execute(
select(models.Message)
.where(models.Message.user_id == user.id)
.order_by(models.Message.timestamp.desc())
.limit(limit)
)
return list(reversed(result.scalars().all()))

9
app/database.py Normal file
View File

@@ -0,0 +1,9 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
import os
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_async_engine(DATABASE_URL, echo=True, future=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()

18
app/main.py Normal file
View File

@@ -0,0 +1,18 @@
from fastapi import FastAPI
from . import database, models, api
from app.api import router as api_router
app = FastAPI()
app.include_router(api_router, prefix="/api")
@app.on_event("startup")
async def startup():
async with database.engine.begin() as conn:
await conn.run_sync(models.Base.metadata.create_all)
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
index_path = os.path.join(dist_path, "index.html")
return FileResponse(index_path)

33
app/models.py Normal file
View File

@@ -0,0 +1,33 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, func
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(String, unique=True, index=True)
username = Column(String, nullable=True)
chats = relationship("Chat", back_populates="owner")
class Chat(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, default="Новый чат")
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="chats")
messages = relationship("Message", back_populates="chat", cascade="all, delete-orphan")
class Message(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
role = Column(String) # 'user' | 'assistant' | 'system'
content = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
chat_id = Column(Integer, ForeignKey("chats.id"))
chat = relationship("Chat", back_populates="messages")

33
app/openrouter_client.py Normal file
View File

@@ -0,0 +1,33 @@
import os
from openai import OpenAI
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
SITE_URL = os.getenv("WEBAPP_URL", "https://llm.byte-mate.xyz")
SITE_NAME = "Telegram LLM Chat"
# инициализируем клиента
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY,
)
async def generate_response(messages):
"""
messages: список сообщений [{'role': 'user'|'assistant'|'system', 'content': str}]
"""
try:
completion = client.chat.completions.create(
# дополнительные заголовки для OpenRouter (не обязательно, но полезно)
extra_headers={
"HTTP-Referer": SITE_URL,
"X-Title": SITE_NAME,
},
model="qwen/qwen3-235b-a22b:free", # ✅ используй модель, которая точно доступна твоему APIключу
messages=messages,
)
return completion.choices[0].message.content
except Exception as e:
# Логи и возврат ошибки
print(f"❌ Ошибка вызова OpenRouter API: {e}")
return "Извините, LLM сейчас недоступен. Попробуйте позже."

35
app/schemas.py Normal file
View File

@@ -0,0 +1,35 @@
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
class MessageBase(BaseModel):
role: str
content: str
class MessageOut(MessageBase):
id: int
created_at: datetime
class Config:
orm_mode = True
class ChatBase(BaseModel):
title: str
class ChatOut(ChatBase):
id: int
messages: List[MessageOut] = []
class Config:
orm_mode = True
class UserOut(BaseModel):
id: int
telegram_id: str
username: Optional[str]
chats: List[ChatOut] = []
class Config:
orm_mode = True