feat: Add review events persistence, version display, and auto-versioning system

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-10-13 14:18:37 +03:00
parent cfba28f913
commit 2db1225618
56 changed files with 750 additions and 436 deletions

1
backend/VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

View File

@@ -552,7 +552,7 @@ class ReviewerAgent:
try:
async for event in self.graph.astream(
initial_state,
stream_mode=["updates"]
stream_mode=["updates", "messages"]
):
event_count += 1
print(f"📨 Event #{event_count} received from graph")
@@ -581,6 +581,28 @@ class ReviewerAgent:
if isinstance(node_data, dict):
final_state = node_data
# Handle 'messages' events (LLM streaming)
elif event_type == 'messages':
print(f" 💬 LLM messages received")
# event_data is a list of messages
if isinstance(event_data, (list, tuple)):
for msg in event_data:
# Check if it's an AIMessage or similar
msg_content = None
if hasattr(msg, 'content'):
msg_content = msg.content
elif isinstance(msg, dict) and 'content' in msg:
msg_content = msg['content']
else:
msg_content = str(msg)
if msg_content and on_event:
print(f" 💬 Sending LLM message: {msg_content[:100]}...")
await on_event({
"type": "llm_message",
"message": msg_content
})
# Handle 'values' events (state snapshots)
elif event_type == 'values':
print(f" 📊 State snapshot received")

View File

@@ -6,9 +6,11 @@ from sqlalchemy import select, func
from sqlalchemy.orm import joinedload
from app.database import get_db
from app.models import Review, Comment, PullRequest
from app.models import Review, Comment, PullRequest, ReviewEvent
from app.schemas.review import ReviewResponse, ReviewList, ReviewStats, PullRequestInfo, CommentResponse
from app.schemas.review_event import ReviewEvent as ReviewEventSchema
from app.agents import ReviewerAgent
from typing import List
router = APIRouter()
@@ -216,3 +218,27 @@ async def get_review_stats(db: AsyncSession = Depends(get_db)):
avg_comments_per_review=round(avg_comments, 2)
)
@router.get("/{review_id}/events", response_model=List[ReviewEventSchema])
async def get_review_events(
review_id: int,
db: AsyncSession = Depends(get_db)
):
"""Get all events for a specific review"""
# Check if review exists
result = await db.execute(select(Review).where(Review.id == review_id))
review = result.scalar_one_or_none()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
# Get events
events_result = await db.execute(
select(ReviewEvent)
.where(ReviewEvent.review_id == review_id)
.order_by(ReviewEvent.created_at)
)
events = events_result.scalars().all()
return events

View File

@@ -118,6 +118,20 @@ async def health_check():
return {"status": "healthy"}
@app.get("/version")
async def get_version():
"""Get backend version"""
try:
version_file = Path(__file__).parent.parent / "VERSION"
if version_file.exists():
version = version_file.read_text().strip()
else:
version = "unknown"
return {"version": version}
except Exception:
return {"version": "unknown"}
@app.websocket("/ws/reviews")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket endpoint for real-time review updates"""

View File

@@ -6,6 +6,7 @@ from app.models.review import Review
from app.models.comment import Comment
from app.models.organization import Organization
from app.models.review_task import ReviewTask
from app.models.review_event import ReviewEvent
__all__ = ["Repository", "PullRequest", "Review", "Comment", "Organization", "ReviewTask"]
__all__ = ["Repository", "PullRequest", "Review", "Comment", "Organization", "ReviewTask", "ReviewEvent"]

View File

@@ -37,6 +37,7 @@ class Review(Base):
# Relationships
pull_request = relationship("PullRequest", back_populates="reviews")
comments = relationship("Comment", back_populates="review", cascade="all, delete-orphan")
events = relationship("ReviewEvent", back_populates="review", cascade="all, delete-orphan", order_by="ReviewEvent.created_at")
def __repr__(self):
return f"<Review(id={self.id}, status={self.status}, pr_id={self.pull_request_id})>"

View File

@@ -0,0 +1,27 @@
"""Review Event model - хранение событий процесса review"""
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base
class ReviewEvent(Base):
"""Событие процесса review"""
__tablename__ = "review_events"
id = Column(Integer, primary_key=True, index=True)
review_id = Column(Integer, ForeignKey("reviews.id", ondelete="CASCADE"), nullable=False, index=True)
event_type = Column(String(50), nullable=False) # agent_step, llm_message, review_started, etc.
step = Column(String(100), nullable=True) # fetch_pr_info, analyze_files, etc.
message = Column(Text, nullable=True)
data = Column(JSON, nullable=True) # Дополнительные данные события
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
# Relationships
review = relationship("Review", back_populates="events")
def __repr__(self):
return f"<ReviewEvent(id={self.id}, review_id={self.review_id}, type={self.event_type})>"

View File

@@ -23,6 +23,10 @@ from app.schemas.streaming import (
ReviewProgressEvent,
StreamEventType
)
from app.schemas.review_event import (
ReviewEvent as ReviewEventSchema,
ReviewEventCreate
)
__all__ = [
"RepositoryCreate",
@@ -40,5 +44,7 @@ __all__ = [
"LLMStreamEvent",
"ReviewProgressEvent",
"StreamEventType",
"ReviewEventSchema",
"ReviewEventCreate",
]

View File

@@ -0,0 +1,29 @@
"""Review Event schemas"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional, Dict, Any
class ReviewEventBase(BaseModel):
"""Base review event schema"""
event_type: str = Field(..., description="Тип события")
step: Optional[str] = Field(None, description="Шаг процесса")
message: Optional[str] = Field(None, description="Сообщение")
data: Optional[Dict[str, Any]] = Field(None, description="Дополнительные данные")
class ReviewEventCreate(ReviewEventBase):
"""Schema for creating review event"""
review_id: int
class ReviewEvent(ReviewEventBase):
"""Review event response schema"""
id: int
review_id: int
created_at: datetime
class Config:
from_attributes = True

View File

@@ -200,6 +200,19 @@ class ReviewTaskWorker:
logger.info(f" 🔔 Broadcasting event: type={event.get('type')}, connections={len(manager.active_connections)}")
# Save event to database
from app.models.review_event import ReviewEvent
db_event = ReviewEvent(
review_id=review.id,
event_type=event.get("type", "agent_update"),
step=event.get("step"),
message=event.get("message"),
data=event
)
db.add(db_event)
await db.commit()
logger.debug(f" 💾 Event saved to DB: {db_event.id}")
# Broadcast to all connected clients
await manager.broadcast(event_data)

View File

@@ -4,7 +4,7 @@
import asyncio
from app.database import engine, Base
from app.models import Organization, ReviewTask, Repository, PullRequest, Review, Comment
from app.models import Organization, ReviewTask, Repository, PullRequest, Review, Comment, ReviewEvent
async def create_tables():
@@ -20,6 +20,7 @@ async def create_tables():
print(" - pull_requests")
print(" - reviews")
print(" - comments")
print(" - review_events")
if __name__ == "__main__":

View File

@@ -0,0 +1,17 @@
-- Migration: Add review_events table
CREATE TABLE IF NOT EXISTS review_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
review_id INTEGER NOT NULL,
event_type VARCHAR(50) NOT NULL,
step VARCHAR(100),
message TEXT,
data JSON,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_review_events_review_id ON review_events(review_id);
CREATE INDEX IF NOT EXISTS idx_review_events_created_at ON review_events(created_at);