feat: Add review events persistence, version display, and auto-versioning system
This commit is contained in:
1
backend/VERSION
Normal file
1
backend/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
0.1.0
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
27
backend/app/models/review_event.py
Normal file
27
backend/app/models/review_event.py
Normal 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})>"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
29
backend/app/schemas/review_event.py
Normal file
29
backend/app/schemas/review_event.py
Normal 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
17
backend/migrations/add_review_events.sql
Normal file
17
backend/migrations/add_review_events.sql
Normal 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);
|
||||
|
||||
Reference in New Issue
Block a user