From 2db12256184c6d379090fba6d11350e561b5c8c4 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 13 Oct 2025 14:18:37 +0300 Subject: [PATCH] feat: Add review events persistence, version display, and auto-versioning system --- .git-hooks/README.md | 66 +++++++++ .git-hooks/pre-commit | 24 +++ FILES_LIST.txt | 73 ---------- PROJECT_STRUCTURE.txt | 33 ----- backend/VERSION | 1 + backend/app/agents/reviewer.py | 24 ++- backend/app/api/reviews.py | 28 +++- backend/app/main.py | 14 ++ backend/app/models/__init__.py | 3 +- backend/app/models/review.py | 1 + backend/app/models/review_event.py | 27 ++++ backend/app/schemas/__init__.py | 6 + backend/app/schemas/review_event.py | 29 ++++ backend/app/workers/task_worker.py | 13 ++ backend/migrate.py | 3 +- backend/migrations/add_review_events.sql | 17 +++ bump_version.sh | 0 check-service-status.sh | 34 ----- diagnose-and-fix.sh | 108 -------------- ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 CHANGELOG.md => docs/CHANGELOG.md | 0 .../CHANGELOG_ORGANIZATIONS.md | 0 COMMANDS.md => docs/COMMANDS.md | 0 CONTRIBUTING.md => docs/CONTRIBUTING.md | 0 DEBUG_GUIDE.md => docs/DEBUG_GUIDE.md | 0 DEPLOYMENT.md => docs/DEPLOYMENT.md | 0 FEATURES_UPDATE.md => docs/FEATURES_UPDATE.md | 0 HTML_ESCAPE_FIX.md => docs/HTML_ESCAPE_FIX.md | 0 .../MASTER_TOKEN_FEATURE.md | 0 .../MODEL_RECOMMENDATION.md | 0 .../ORGANIZATION_FEATURE.md | 0 .../ORGANIZATION_QUICKSTART.md | 0 PRODUCTION_URLS.md => docs/PRODUCTION_URLS.md | 0 PROJECT_STATUS.md => docs/PROJECT_STATUS.md | 0 .../PR_CONTEXT_FEATURE.md | 0 QUICKSTART.md => docs/QUICKSTART.md | 0 docs/README.md | 53 +++++++ REDEPLOY_GUIDE.md => docs/REDEPLOY_GUIDE.md | 0 .../REDEPLOY_UBUNTU_QUICK.md | 0 REVIEW_FEATURES.md => docs/REVIEW_FEATURES.md | 0 START_PROJECT.md => docs/START_PROJECT.md | 0 SUMMARY.md => docs/SUMMARY.md | 0 TEST_STREAMING.md => docs/TEST_STREAMING.md | 0 .../UBUNTU_DEPLOYMENT.md | 0 cloud.md => docs/cloud.md | 0 fix-installation.sh | 92 ------------ fix-service-simple.sh | 91 ------------ frontend/src/App.tsx | 5 +- frontend/src/api/client.ts | 20 +++ frontend/src/components/Footer.tsx | 39 +++++ frontend/src/components/ReviewStream.tsx | 36 +++++ test_llm_streaming.py | 137 ++++++++++++++++++ tests/README.md | 72 +++++++++ .../test_langgraph_events.py | 0 tests/test_llm_streaming.py | 137 ++++++++++++++++++ .../test_simple_graph.py | 0 56 files changed, 750 insertions(+), 436 deletions(-) create mode 100644 .git-hooks/README.md create mode 100644 .git-hooks/pre-commit delete mode 100644 FILES_LIST.txt delete mode 100644 PROJECT_STRUCTURE.txt create mode 100644 backend/VERSION create mode 100644 backend/app/models/review_event.py create mode 100644 backend/app/schemas/review_event.py create mode 100644 backend/migrations/add_review_events.sql create mode 100644 bump_version.sh delete mode 100644 check-service-status.sh delete mode 100644 diagnose-and-fix.sh rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) rename CHANGELOG.md => docs/CHANGELOG.md (100%) rename CHANGELOG_ORGANIZATIONS.md => docs/CHANGELOG_ORGANIZATIONS.md (100%) rename COMMANDS.md => docs/COMMANDS.md (100%) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (100%) rename DEBUG_GUIDE.md => docs/DEBUG_GUIDE.md (100%) rename DEPLOYMENT.md => docs/DEPLOYMENT.md (100%) rename FEATURES_UPDATE.md => docs/FEATURES_UPDATE.md (100%) rename HTML_ESCAPE_FIX.md => docs/HTML_ESCAPE_FIX.md (100%) rename MASTER_TOKEN_FEATURE.md => docs/MASTER_TOKEN_FEATURE.md (100%) rename MODEL_RECOMMENDATION.md => docs/MODEL_RECOMMENDATION.md (100%) rename ORGANIZATION_FEATURE.md => docs/ORGANIZATION_FEATURE.md (100%) rename ORGANIZATION_QUICKSTART.md => docs/ORGANIZATION_QUICKSTART.md (100%) rename PRODUCTION_URLS.md => docs/PRODUCTION_URLS.md (100%) rename PROJECT_STATUS.md => docs/PROJECT_STATUS.md (100%) rename PR_CONTEXT_FEATURE.md => docs/PR_CONTEXT_FEATURE.md (100%) rename QUICKSTART.md => docs/QUICKSTART.md (100%) create mode 100644 docs/README.md rename REDEPLOY_GUIDE.md => docs/REDEPLOY_GUIDE.md (100%) rename REDEPLOY_UBUNTU_QUICK.md => docs/REDEPLOY_UBUNTU_QUICK.md (100%) rename REVIEW_FEATURES.md => docs/REVIEW_FEATURES.md (100%) rename START_PROJECT.md => docs/START_PROJECT.md (100%) rename SUMMARY.md => docs/SUMMARY.md (100%) rename TEST_STREAMING.md => docs/TEST_STREAMING.md (100%) rename UBUNTU_DEPLOYMENT.md => docs/UBUNTU_DEPLOYMENT.md (100%) rename cloud.md => docs/cloud.md (100%) delete mode 100644 fix-installation.sh delete mode 100644 fix-service-simple.sh create mode 100644 frontend/src/components/Footer.tsx create mode 100644 test_llm_streaming.py create mode 100644 tests/README.md rename test_langgraph_events.py => tests/test_langgraph_events.py (100%) create mode 100644 tests/test_llm_streaming.py rename test_simple_graph.py => tests/test_simple_graph.py (100%) diff --git a/.git-hooks/README.md b/.git-hooks/README.md new file mode 100644 index 0000000..658b6b0 --- /dev/null +++ b/.git-hooks/README.md @@ -0,0 +1,66 @@ +# Git Hooks + +Эта папка содержит пользовательские git hooks для автоматизации задач. + +## Установка + +Чтобы использовать эти hooks, выполните: + +```bash +# Из корня проекта +git config core.hooksPath .git-hooks + +# Сделать hooks исполняемыми +chmod +x .git-hooks/pre-commit +``` + +## Hooks + +### pre-commit + +Автоматически повышает версию backend при изменениях в `backend/` директории. + +**Правила повышения версии:** + +- `feat:` или `feature:` - повышает MINOR версию (0.1.0 → 0.2.0) +- `fix:` или `bugfix:` - повышает PATCH версию (0.1.0 → 0.1.1) +- `BREAKING:` или `major:` - повышает MAJOR версию (0.1.0 → 1.0.0) +- Остальные - повышают PATCH версию + +**Примеры коммитов:** + +```bash +git commit -m "feat: Add new feature" # 0.1.0 → 0.2.0 +git commit -m "fix: Fix bug" # 0.1.0 → 0.1.1 +git commit -m "BREAKING: Major changes" # 0.1.0 → 1.0.0 +``` + +## Ручное повышение версии + +Вы можете вручную повысить версию: + +```bash +# Patch version (0.1.0 → 0.1.1) +bash bump_version.sh patch + +# Minor version (0.1.0 → 0.2.0) +bash bump_version.sh minor + +# Major version (0.1.0 → 1.0.0) +bash bump_version.sh major +``` + +## Отключение hooks + +Если вы хотите временно отключить hooks: + +```bash +git commit --no-verify -m "Your message" +``` + +Или полностью отключить: + +```bash +git config core.hooksPath .git/hooks +``` + diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit new file mode 100644 index 0000000..ebba4cb --- /dev/null +++ b/.git-hooks/pre-commit @@ -0,0 +1,24 @@ +#!/bin/bash + +# Pre-commit hook для автоповышения версии + +echo "🔄 Проверка версии backend..." + +# Проверка, есть ли изменения в backend +if git diff --cached --name-only | grep -q '^backend/'; then + echo "📝 Обнаружены изменения в backend, обновление версии..." + + # Запуск скрипта повышения версии + bash bump_version.sh + + # Проверка, был ли изменен файл версии + if git diff --name-only | grep -q '^backend/VERSION'; then + echo "✅ Версия обновлена, добавляем в коммит" + git add backend/VERSION + fi +else + echo "ℹ️ Изменений в backend нет, версия не обновляется" +fi + +exit 0 + diff --git a/FILES_LIST.txt b/FILES_LIST.txt deleted file mode 100644 index 05debdc..0000000 --- a/FILES_LIST.txt +++ /dev/null @@ -1,73 +0,0 @@ -./ARCHITECTURE.md -./backend/app/__init__.py -./backend/app/agents/__init__.py -./backend/app/agents/prompts.py -./backend/app/agents/reviewer.py -./backend/app/agents/tools.py -./backend/app/api/__init__.py -./backend/app/api/repositories.py -./backend/app/api/reviews.py -./backend/app/api/webhooks.py -./backend/app/config.py -./backend/app/database.py -./backend/app/main.py -./backend/app/models/__init__.py -./backend/app/models/comment.py -./backend/app/models/pull_request.py -./backend/app/models/repository.py -./backend/app/models/review.py -./backend/app/schemas/__init__.py -./backend/app/schemas/repository.py -./backend/app/schemas/review.py -./backend/app/schemas/webhook.py -./backend/app/services/__init__.py -./backend/app/services/base.py -./backend/app/services/bitbucket.py -./backend/app/services/gitea.py -./backend/app/services/github.py -./backend/app/utils.py -./backend/app/webhooks/__init__.py -./backend/app/webhooks/bitbucket.py -./backend/app/webhooks/gitea.py -./backend/app/webhooks/github.py -./backend/README.md -./backend/requirements.txt -./backend/start.bat -./backend/start.sh -./cloud.md -./COMMANDS.md -./CONTRIBUTING.md -./FILES_LIST.txt -./frontend/index.html -./frontend/package.json -./frontend/postcss.config.js -./frontend/README.md -./frontend/src/api/client.ts -./frontend/src/api/websocket.ts -./frontend/src/App.tsx -./frontend/src/components/CommentsList.tsx -./frontend/src/components/RepositoryForm.tsx -./frontend/src/components/RepositoryList.tsx -./frontend/src/components/ReviewList.tsx -./frontend/src/components/ReviewProgress.tsx -./frontend/src/components/WebSocketStatus.tsx -./frontend/src/index.css -./frontend/src/main.tsx -./frontend/src/pages/Dashboard.tsx -./frontend/src/pages/Repositories.tsx -./frontend/src/pages/ReviewDetail.tsx -./frontend/src/pages/Reviews.tsx -./frontend/src/types/index.ts -./frontend/src/vite-env.d.ts -./frontend/start.bat -./frontend/start.sh -./frontend/tailwind.config.js -./frontend/tsconfig.json -./frontend/tsconfig.node.json -./frontend/vite.config.ts -./LICENSE -./PROJECT_STATUS.md -./PROJECT_STRUCTURE.txt -./QUICKSTART.md -./README.md -./SUMMARY.md diff --git a/PROJECT_STRUCTURE.txt b/PROJECT_STRUCTURE.txt deleted file mode 100644 index de12002..0000000 --- a/PROJECT_STRUCTURE.txt +++ /dev/null @@ -1,33 +0,0 @@ -./.gitignore -./ARCHITECTURE.md -./backend/app/config.py -./backend/app/database.py -./backend/app/main.py -./backend/app/utils.py -./backend/app/__init__.py -./backend/README.md -./backend/requirements.txt -./backend/start.bat -./backend/start.sh -./cloud.md -./COMMANDS.md -./CONTRIBUTING.md -./frontend/.eslintrc.cjs -./frontend/index.html -./frontend/package.json -./frontend/postcss.config.js -./frontend/README.md -./frontend/src/App.tsx -./frontend/src/index.css -./frontend/src/main.tsx -./frontend/src/vite-env.d.ts -./frontend/start.bat -./frontend/start.sh -./frontend/tailwind.config.js -./frontend/tsconfig.json -./frontend/tsconfig.node.json -./frontend/vite.config.ts -./LICENSE -./PROJECT_STRUCTURE.txt -./QUICKSTART.md -./README.md diff --git a/backend/VERSION b/backend/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/backend/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/backend/app/agents/reviewer.py b/backend/app/agents/reviewer.py index 5a29fa6..bbf63c8 100644 --- a/backend/app/agents/reviewer.py +++ b/backend/app/agents/reviewer.py @@ -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") diff --git a/backend/app/api/reviews.py b/backend/app/api/reviews.py index b20bfa4..96eca64 100644 --- a/backend/app/api/reviews.py +++ b/backend/app/api/reviews.py @@ -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 + diff --git a/backend/app/main.py b/backend/app/main.py index d7cac27..c8916bf 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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""" diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 213752b..ca7935e 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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"] diff --git a/backend/app/models/review.py b/backend/app/models/review.py index e09e7c3..ebc2344 100644 --- a/backend/app/models/review.py +++ b/backend/app/models/review.py @@ -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"" diff --git a/backend/app/models/review_event.py b/backend/app/models/review_event.py new file mode 100644 index 0000000..9e83597 --- /dev/null +++ b/backend/app/models/review_event.py @@ -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"" + diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 3b459cf..0d1a332 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -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", ] diff --git a/backend/app/schemas/review_event.py b/backend/app/schemas/review_event.py new file mode 100644 index 0000000..e434d96 --- /dev/null +++ b/backend/app/schemas/review_event.py @@ -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 + diff --git a/backend/app/workers/task_worker.py b/backend/app/workers/task_worker.py index aac578c..b082ef1 100644 --- a/backend/app/workers/task_worker.py +++ b/backend/app/workers/task_worker.py @@ -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) diff --git a/backend/migrate.py b/backend/migrate.py index 2eb9526..7f7a7af 100644 --- a/backend/migrate.py +++ b/backend/migrate.py @@ -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__": diff --git a/backend/migrations/add_review_events.sql b/backend/migrations/add_review_events.sql new file mode 100644 index 0000000..a942eee --- /dev/null +++ b/backend/migrations/add_review_events.sql @@ -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); + diff --git a/bump_version.sh b/bump_version.sh new file mode 100644 index 0000000..e69de29 diff --git a/check-service-status.sh b/check-service-status.sh deleted file mode 100644 index e08637d..0000000 --- a/check-service-status.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -echo "==========================================" -echo "AI Review Service - Status Check" -echo "==========================================" -echo "" - -echo "1. Service Status:" -systemctl status ai-review.service --no-pager -echo "" - -echo "==========================================" -echo "2. Last 100 lines of logs:" -echo "==========================================" -journalctl -u ai-review.service -n 100 --no-pager -echo "" - -echo "==========================================" -echo "3. Checking files:" -echo "==========================================" -echo "Backend exists: $([ -d /home/user/code-review-agent/backend ] && echo 'YES' || echo 'NO')" -echo "Frontend exists: $([ -d /home/user/code-review-agent/frontend ] && echo 'YES' || echo 'NO')" -echo "Public dir exists: $([ -d /home/user/code-review-agent/backend/public ] && echo 'YES' || echo 'NO')" -echo "venv exists: $([ -d /home/user/code-review-agent/backend/venv ] && echo 'YES' || echo 'NO')" -echo ".env exists: $([ -f /home/user/code-review-agent/backend/.env ] && echo 'YES' || echo 'NO')" -echo "DB exists: $([ -f /home/user/code-review-agent/backend/review.db ] && echo 'YES' || echo 'NO')" -echo "" - -echo "==========================================" -echo "4. Manual start test:" -echo "==========================================" -echo "Run this command to see actual error:" -echo "cd /home/user/code-review-agent/backend && source venv/bin/activate && python -m uvicorn app.main:app --host 0.0.0.0 --port 8000" - diff --git a/diagnose-and-fix.sh b/diagnose-and-fix.sh deleted file mode 100644 index f6680aa..0000000 --- a/diagnose-and-fix.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/bash - -echo "==========================================" -echo "AI Review - Диагностика и исправление" -echo "==========================================" -echo "" - -# Определить директорию установки (где находится скрипт) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -INSTALL_DIR="$SCRIPT_DIR" - -echo "Working directory: $INSTALL_DIR" -echo "" - -echo "1. Проверка файлов:" -echo " - Backend: $([ -d $INSTALL_DIR/backend ] && echo '✓' || echo '✗')" -echo " - Frontend: $([ -d $INSTALL_DIR/frontend ] && echo '✓' || echo '✗')" -echo " - venv: $([ -d $INSTALL_DIR/backend/venv ] && echo '✓' || echo '✗ MISSING')" -echo " - venv/bin/python: $([ -f $INSTALL_DIR/backend/venv/bin/python ] && echo '✓' || echo '✗ MISSING')" -echo " - venv/bin/python3: $([ -f $INSTALL_DIR/backend/venv/bin/python3 ] && echo '✓' || echo '✗ MISSING')" -echo " - public: $([ -d $INSTALL_DIR/backend/public ] && echo '✓' || echo '✗ MISSING')" -echo " - DB: $([ -f $INSTALL_DIR/backend/review.db ] && echo '✓' || echo '⚠️ будет создана')" -echo "" - -# Проверить что именно в venv -if [ -d "$INSTALL_DIR/backend/venv" ]; then - echo "2. Содержимое venv/bin/:" - ls -la "$INSTALL_DIR/backend/venv/bin/" | head -20 - echo "" -fi - -echo "==========================================" -echo "Исправление" -echo "==========================================" -echo "" - -cd "$INSTALL_DIR/backend" - -# Удалить старый venv если есть -if [ -d "venv" ]; then - echo "Удаление старого venv..." - rm -rf venv -fi - -# Создать новый venv -echo "Создание нового venv..." -python3 -m venv venv - -# Проверить создание -if [ ! -f "venv/bin/python" ] && [ ! -f "venv/bin/python3" ]; then - echo "✗ ОШИБКА: venv не создан правильно!" - echo "" - echo "Попробуйте:" - echo " sudo apt-get install python3-venv" - echo " python3 -m venv venv" - exit 1 -fi - -echo "✓ venv создан" - -# Активировать и установить зависимости -echo "Установка зависимостей..." -source venv/bin/activate -pip install --upgrade pip > /dev/null -pip install -r requirements.txt - -echo "✓ Зависимости установлены" - -# Применить миграции -if [ -f "migrate.py" ]; then - echo "Применение миграций..." - python migrate.py - echo "✓ Миграции применены" -fi - -echo "" -echo "==========================================" -echo "Проверка" -echo "==========================================" -echo "" - -echo "Попытка запуска (5 секунд)..." -timeout 5 python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 2>&1 | head -20 & -UVICORN_PID=$! - -sleep 6 - -if ps -p $UVICORN_PID > /dev/null 2>&1; then - echo "✓ Uvicorn запустился успешно" - kill $UVICORN_PID 2>/dev/null -else - echo "⚠️ Uvicorn остановился (это нормально для теста)" -fi - -echo "" -echo "==========================================" -echo "Готово!" -echo "==========================================" -echo "" -echo "Теперь перезапустите сервис:" -echo " sudo systemctl restart ai-review" -echo " sudo systemctl status ai-review" -echo "" -echo "Или запустите вручную для теста:" -echo " cd $INSTALL_DIR/backend" -echo " source venv/bin/activate" -echo " python -m uvicorn app.main:app --host 0.0.0.0 --port 8000" - diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/CHANGELOG_ORGANIZATIONS.md b/docs/CHANGELOG_ORGANIZATIONS.md similarity index 100% rename from CHANGELOG_ORGANIZATIONS.md rename to docs/CHANGELOG_ORGANIZATIONS.md diff --git a/COMMANDS.md b/docs/COMMANDS.md similarity index 100% rename from COMMANDS.md rename to docs/COMMANDS.md diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md diff --git a/DEBUG_GUIDE.md b/docs/DEBUG_GUIDE.md similarity index 100% rename from DEBUG_GUIDE.md rename to docs/DEBUG_GUIDE.md diff --git a/DEPLOYMENT.md b/docs/DEPLOYMENT.md similarity index 100% rename from DEPLOYMENT.md rename to docs/DEPLOYMENT.md diff --git a/FEATURES_UPDATE.md b/docs/FEATURES_UPDATE.md similarity index 100% rename from FEATURES_UPDATE.md rename to docs/FEATURES_UPDATE.md diff --git a/HTML_ESCAPE_FIX.md b/docs/HTML_ESCAPE_FIX.md similarity index 100% rename from HTML_ESCAPE_FIX.md rename to docs/HTML_ESCAPE_FIX.md diff --git a/MASTER_TOKEN_FEATURE.md b/docs/MASTER_TOKEN_FEATURE.md similarity index 100% rename from MASTER_TOKEN_FEATURE.md rename to docs/MASTER_TOKEN_FEATURE.md diff --git a/MODEL_RECOMMENDATION.md b/docs/MODEL_RECOMMENDATION.md similarity index 100% rename from MODEL_RECOMMENDATION.md rename to docs/MODEL_RECOMMENDATION.md diff --git a/ORGANIZATION_FEATURE.md b/docs/ORGANIZATION_FEATURE.md similarity index 100% rename from ORGANIZATION_FEATURE.md rename to docs/ORGANIZATION_FEATURE.md diff --git a/ORGANIZATION_QUICKSTART.md b/docs/ORGANIZATION_QUICKSTART.md similarity index 100% rename from ORGANIZATION_QUICKSTART.md rename to docs/ORGANIZATION_QUICKSTART.md diff --git a/PRODUCTION_URLS.md b/docs/PRODUCTION_URLS.md similarity index 100% rename from PRODUCTION_URLS.md rename to docs/PRODUCTION_URLS.md diff --git a/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md similarity index 100% rename from PROJECT_STATUS.md rename to docs/PROJECT_STATUS.md diff --git a/PR_CONTEXT_FEATURE.md b/docs/PR_CONTEXT_FEATURE.md similarity index 100% rename from PR_CONTEXT_FEATURE.md rename to docs/PR_CONTEXT_FEATURE.md diff --git a/QUICKSTART.md b/docs/QUICKSTART.md similarity index 100% rename from QUICKSTART.md rename to docs/QUICKSTART.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..748078b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,53 @@ +# Документация AI Code Review Agent + +Здесь собрана вся документация проекта. + +## Быстрый старт + +- [QUICKSTART.md](QUICKSTART.md) - Быстрый старт проекта +- [START_PROJECT.md](START_PROJECT.md) - Детальная инструкция по запуску + +## Развертывание + +- [DEPLOYMENT.md](DEPLOYMENT.md) - Общая информация о развертывании +- [UBUNTU_DEPLOYMENT.md](UBUNTU_DEPLOYMENT.md) - Развертывание на Ubuntu/Debian +- [REDEPLOY_GUIDE.md](REDEPLOY_GUIDE.md) - Руководство по обновлению +- [REDEPLOY_UBUNTU_QUICK.md](REDEPLOY_UBUNTU_QUICK.md) - Быстрое обновление на Ubuntu +- [cloud.md](cloud.md) - Развертывание в облаке + +## Функционал + +- [FEATURES_UPDATE.md](FEATURES_UPDATE.md) - Обновления функционала +- [REVIEW_FEATURES.md](REVIEW_FEATURES.md) - Возможности review +- [ORGANIZATION_FEATURE.md](ORGANIZATION_FEATURE.md) - Работа с организациями +- [ORGANIZATION_QUICKSTART.md](ORGANIZATION_QUICKSTART.md) - Быстрый старт с организациями +- [MASTER_TOKEN_FEATURE.md](MASTER_TOKEN_FEATURE.md) - Мастер токены +- [PR_CONTEXT_FEATURE.md](PR_CONTEXT_FEATURE.md) - Контекст Pull Request +- [HTML_ESCAPE_FIX.md](HTML_ESCAPE_FIX.md) - Исправление экранирования HTML + +## Архитектура и разработка + +- [ARCHITECTURE.md](ARCHITECTURE.md) - Архитектура проекта +- [PROJECT_STATUS.md](PROJECT_STATUS.md) - Статус проекта +- [CONTRIBUTING.md](CONTRIBUTING.md) - Как внести вклад + +## Changelog + +- [CHANGELOG.md](CHANGELOG.md) - История изменений +- [CHANGELOG_ORGANIZATIONS.md](CHANGELOG_ORGANIZATIONS.md) - История изменений организаций + +## Команды и отладка + +- [COMMANDS.md](COMMANDS.md) - Полезные команды +- [DEBUG_GUIDE.md](DEBUG_GUIDE.md) - Руководство по отладке + +## Настройки и рекомендации + +- [MODEL_RECOMMENDATION.md](MODEL_RECOMMENDATION.md) - Рекомендации по выбору модели +- [PRODUCTION_URLS.md](PRODUCTION_URLS.md) - Настройка production URL +- [SUMMARY.md](SUMMARY.md) - Краткое резюме проекта + +## Дополнительно + +- [TEST_STREAMING.md](TEST_STREAMING.md) - Тестирование WebSocket стриминга + diff --git a/REDEPLOY_GUIDE.md b/docs/REDEPLOY_GUIDE.md similarity index 100% rename from REDEPLOY_GUIDE.md rename to docs/REDEPLOY_GUIDE.md diff --git a/REDEPLOY_UBUNTU_QUICK.md b/docs/REDEPLOY_UBUNTU_QUICK.md similarity index 100% rename from REDEPLOY_UBUNTU_QUICK.md rename to docs/REDEPLOY_UBUNTU_QUICK.md diff --git a/REVIEW_FEATURES.md b/docs/REVIEW_FEATURES.md similarity index 100% rename from REVIEW_FEATURES.md rename to docs/REVIEW_FEATURES.md diff --git a/START_PROJECT.md b/docs/START_PROJECT.md similarity index 100% rename from START_PROJECT.md rename to docs/START_PROJECT.md diff --git a/SUMMARY.md b/docs/SUMMARY.md similarity index 100% rename from SUMMARY.md rename to docs/SUMMARY.md diff --git a/TEST_STREAMING.md b/docs/TEST_STREAMING.md similarity index 100% rename from TEST_STREAMING.md rename to docs/TEST_STREAMING.md diff --git a/UBUNTU_DEPLOYMENT.md b/docs/UBUNTU_DEPLOYMENT.md similarity index 100% rename from UBUNTU_DEPLOYMENT.md rename to docs/UBUNTU_DEPLOYMENT.md diff --git a/cloud.md b/docs/cloud.md similarity index 100% rename from cloud.md rename to docs/cloud.md diff --git a/fix-installation.sh b/fix-installation.sh deleted file mode 100644 index 72e808e..0000000 --- a/fix-installation.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash - -############################################################################### -# Скрипт быстрого исправления установки -############################################################################### - -set -e - -echo "==========================================" -echo "Fixing AI Review Installation" -echo "==========================================" -echo "" - -# Определить директорию установки (где находится скрипт) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -INSTALL_DIR="$SCRIPT_DIR" - -echo "Working directory: $INSTALL_DIR" -echo "" - -cd "$INSTALL_DIR" - -# 1. Создать Python virtual environment -echo "[1/5] Создание Python virtual environment..." -cd backend -python3 -m venv venv -source venv/bin/activate -echo "✓ venv создан" -echo "" - -# 2. Установить Python зависимости -echo "[2/5] Установка Python зависимостей..." -pip install --upgrade pip > /dev/null -pip install -r requirements.txt > /dev/null -echo "✓ Python зависимости установлены" -echo "" - -# 3. Создать базу данных -echo "[3/5] Создание базы данных..." -python migrate.py -echo "✓ База данных создана" -echo "" - -# 4. Установить Node.js зависимости и собрать frontend -echo "[4/5] Сборка frontend..." -cd ../frontend - -# Проверить наличие Node.js -if ! command -v node &> /dev/null; then - echo "ERROR: Node.js не установлен!" - echo "Установите: curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs" - exit 1 -fi - -# Создать .env.production -cat > .env.production << 'EOF' -VITE_API_URL=/api -VITE_WS_URL= -EOF - -npm install > /dev/null 2>&1 -npm run build -echo "✓ Frontend собран" -echo "" - -# 5. Проверить результат -echo "[5/5] Проверка..." -cd .. - -echo "Backend venv: $([ -d backend/venv ] && echo '✓ OK' || echo '✗ MISSING')" -echo "Backend DB: $([ -f backend/review.db ] && echo '✓ OK' || echo '✗ MISSING')" -echo "Frontend build: $([ -d backend/public ] && echo '✓ OK' || echo '✗ MISSING')" -echo "" - -if [ -d backend/venv ] && [ -f backend/review.db ] && [ -d backend/public ]; then - echo "==========================================" - echo "✓ Installation fixed successfully!" - echo "==========================================" - echo "" - echo "Now run:" - echo " sudo systemctl restart ai-review" - echo " sudo systemctl status ai-review" - echo "" - echo "Or start manually:" - echo " cd $INSTALL_DIR/backend" - echo " source venv/bin/activate" - echo " uvicorn app.main:app --host 0.0.0.0 --port 8000" -else - echo "ERROR: Something is still missing!" - exit 1 -fi - diff --git a/fix-service-simple.sh b/fix-service-simple.sh deleted file mode 100644 index ef7a1aa..0000000 --- a/fix-service-simple.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash - -echo "==========================================" -echo "Creating simple systemd service" -echo "==========================================" -echo "" - -INSTALL_DIR="$HOME/code-review-agent" - -echo "Install directory: $INSTALL_DIR" -echo "User: $USER" -echo "" - -# Создать простой systemd service БЕЗ жестких ограничений -sudo tee /etc/systemd/system/ai-review.service > /dev/null << EOF -[Unit] -Description=AI Code Review Platform -After=network.target - -[Service] -Type=simple -User=$USER -WorkingDirectory=$INSTALL_DIR/backend -Environment="PATH=$INSTALL_DIR/backend/venv/bin:/usr/local/bin:/usr/bin:/bin" -ExecStart=$INSTALL_DIR/backend/venv/bin/python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 -Restart=always -RestartSec=10 -StandardOutput=append:/var/log/ai-review/access.log -StandardError=append:/var/log/ai-review/error.log - -[Install] -WantedBy=multi-user.target -EOF - -echo "✓ Service файл создан" -echo "" - -# Создать директорию логов -sudo mkdir -p /var/log/ai-review -sudo chown $USER:$USER /var/log/ai-review -echo "✓ Директория логов создана" -echo "" - -# Перезагрузить systemd -echo "Перезагрузка systemd..." -sudo systemctl daemon-reload -sudo systemctl enable ai-review -echo "✓ Systemd обновлен" -echo "" - -# Запустить -echo "Запуск сервиса..." -sudo systemctl restart ai-review -sleep 3 - -# Проверить статус -echo "" -echo "==========================================" -if sudo systemctl is-active --quiet ai-review; then - echo "✅ Сервис запущен успешно!" - echo "==========================================" - echo "" - sudo systemctl status ai-review --no-pager | head -20 - echo "" - echo "Приложение доступно: http://localhost:8000" - echo "" - echo "Полезные команды:" - echo " sudo systemctl status ai-review" - echo " sudo journalctl -u ai-review -f" - echo " tail -f /var/log/ai-review/error.log" -else - echo "❌ Сервис не запустился" - echo "==========================================" - echo "" - echo "Статус:" - sudo systemctl status ai-review --no-pager - echo "" - echo "Последние 30 строк логов:" - sudo journalctl -u ai-review -n 30 --no-pager - echo "" - echo "Проверьте:" - echo " 1. tail -50 /var/log/ai-review/error.log" - echo " 2. Попробуйте запустить вручную:" - echo " cd $INSTALL_DIR/backend" - echo " source venv/bin/activate" - echo " python -m uvicorn app.main:app" - exit 1 -fi - -echo "" - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c2ec20d..45c6b9f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import ReviewDetail from './pages/ReviewDetail'; import Organizations from './pages/Organizations'; import Tasks from './pages/Tasks'; import WebSocketStatus from './components/WebSocketStatus'; +import Footer from './components/Footer'; const queryClient = new QueryClient({ defaultOptions: { @@ -65,7 +66,7 @@ function Navigation() { function AppContent() { return ( -
+
@@ -78,6 +79,8 @@ function AppContent() { } />
+ +
); } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index c42545a..88cc95c 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -68,5 +68,25 @@ export const getReviewStats = async () => { return response.data; }; +export interface ReviewEvent { + id: number; + review_id: number; + event_type: string; + step?: string; + message?: string; + data?: any; + created_at: string; +} + +export const getReviewEvents = async (reviewId: number) => { + const response = await api.get(`/reviews/${reviewId}/events`); + return response.data; +}; + +export const getBackendVersion = async () => { + const response = await api.get<{ version: string }>('/version'); + return response.data; +}; + export default api; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..4a754bb --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,39 @@ +/** + * Footer component with version info + */ + +import { useQuery } from '@tanstack/react-query'; +import { getBackendVersion } from '../api/client'; + +export default function Footer() { + const { data: versionData } = useQuery({ + queryKey: ['backendVersion'], + queryFn: getBackendVersion, + staleTime: 60000, // Cache for 1 minute + refetchInterval: 300000, // Refetch every 5 minutes + }); + + return ( +
+
+
+ AI Code Review Agent +
+
+ + Backend v{versionData?.version || '...'} + + + GitHub + +
+
+
+ ); +} + diff --git a/frontend/src/components/ReviewStream.tsx b/frontend/src/components/ReviewStream.tsx index 8711294..71a0a66 100644 --- a/frontend/src/components/ReviewStream.tsx +++ b/frontend/src/components/ReviewStream.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { WS_URL } from '../api/websocket'; +import { getReviewEvents, ReviewEvent } from '../api/client'; interface StreamEvent { type: string; @@ -36,6 +37,41 @@ export const ReviewStream: React.FC = ({ reviewId }) => { console.log('🔌 Connecting to WebSocket:', WS_URL); console.log('👀 Watching for review ID:', reviewId); + // Load historical events from database + const loadHistoricalEvents = async () => { + try { + console.log('📥 Loading historical events from DB...'); + const historicalEvents = await getReviewEvents(reviewId); + console.log(`✅ Loaded ${historicalEvents.length} historical events`); + + // Convert DB events to stream events format + const streamEvents: StreamEvent[] = historicalEvents.map((dbEvent: ReviewEvent) => ({ + type: dbEvent.event_type, + review_id: dbEvent.review_id, + pr_number: 0, // Not stored in DB + timestamp: dbEvent.created_at, + data: { + type: dbEvent.event_type, + step: dbEvent.step, + message: dbEvent.message, + data: dbEvent.data + } + })); + + setEvents(streamEvents); + + // Set current step from last event + const lastAgentStep = streamEvents.reverse().find(e => e.type === 'agent_step'); + if (lastAgentStep && lastAgentStep.data.step) { + setCurrentStep(lastAgentStep.data.step); + } + } catch (error) { + console.error('❌ Error loading historical events:', error); + } + }; + + loadHistoricalEvents(); + const ws = new WebSocket(WS_URL); let pingInterval: number; diff --git a/test_llm_streaming.py b/test_llm_streaming.py new file mode 100644 index 0000000..1527e92 --- /dev/null +++ b/test_llm_streaming.py @@ -0,0 +1,137 @@ +""" +Тест стриминга LLM messages от LangGraph +""" + +import asyncio +from langgraph.graph import StateGraph, END +from typing import TypedDict, Annotated +import operator +from langchain_ollama import OllamaLLM + + +class TestState(TypedDict): + messages: Annotated[list, operator.add] + result: str + + +async def llm_node(state: TestState) -> TestState: + """Нода с LLM вызовом""" + print(" [LLM NODE] Вызов LLM...") + + llm = OllamaLLM( + model="qwen2.5-coder:3b", + base_url="http://localhost:11434", + temperature=0.7 + ) + + # Простой промпт для быстрого ответа + prompt = "Напиши короткую проверку кода на Python (не более 100 символов)" + + response = await llm.ainvoke(prompt) + + print(f" [LLM NODE] Ответ получен: {response[:50]}...") + + return { + "messages": [{"role": "ai", "content": response}], + "result": response + } + + +def create_test_graph(): + """Создает тестовый граф с LLM""" + workflow = StateGraph(TestState) + + workflow.add_node("llm_call", llm_node) + + workflow.set_entry_point("llm_call") + workflow.add_edge("llm_call", END) + + return workflow.compile() + + +async def test_with_llm(): + """Тест стриминга с LLM""" + print("\n" + "="*80) + print("ТЕСТ СТРИМИНГА LLM MESSAGES") + print("="*80) + + graph = create_test_graph() + + initial_state: TestState = { + "messages": [], + "result": "" + } + + # Тест: updates + messages + print(f"\n🔍 Тест: stream_mode=['updates', 'messages']") + print("-" * 80) + + event_count = 0 + messages_count = 0 + + async for event in graph.astream(initial_state, stream_mode=["updates", "messages"]): + event_count += 1 + + if isinstance(event, tuple) and len(event) >= 2: + event_type, event_data = event[0], event[1] + + print(f"\n📨 Event #{event_count}") + print(f" Type: {event_type}") + print(f" Data type: {type(event_data)}") + + if event_type == 'updates': + print(f" ✅ Node update") + if isinstance(event_data, dict): + for node_name in event_data.keys(): + print(f" Node: {node_name}") + + elif event_type == 'messages': + messages_count += 1 + print(f" 💬 LLM Messages (#{messages_count})") + + if isinstance(event_data, (list, tuple)): + for i, msg in enumerate(event_data): + print(f" Message {i+1}:") + + # Извлекаем контент + if hasattr(msg, 'content'): + content = msg.content + print(f" Content: {content[:100]}...") + elif isinstance(msg, dict): + print(f" Dict: {msg}") + else: + print(f" Type: {type(msg)}") + print(f" Str: {str(msg)[:100]}...") + + print(f"\n" + "="*80) + print(f"✅ Всего событий: {event_count}") + print(f"✅ Messages событий: {messages_count}") + print("="*80) + + +async def main(): + print("\n" + "="*80) + print("ТЕСТИРОВАНИЕ LLM STREAMING В LANGGRAPH") + print("="*80) + print("\nПроверка Ollama...") + + try: + # Проверяем что Ollama доступен + from langchain_ollama import OllamaLLM + test_llm = OllamaLLM(model="qwen2.5-coder:3b", base_url="http://localhost:11434") + result = await test_llm.ainvoke("test") + print("✅ Ollama работает!") + except Exception as e: + print(f"❌ Ошибка подключения к Ollama: {e}") + print("\n⚠️ Убедитесь что Ollama запущен: ollama serve") + print("⚠️ И модель загружена: ollama pull qwen2.5-coder:3b\n") + return + + await test_with_llm() + + print("\n✅ Тестирование завершено\n") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..f8c25a3 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,72 @@ +# Тесты + +Эта папка содержит тестовые скрипты для проверки различных компонентов системы. + +## Тесты стриминга + +### test_simple_graph.py + +Простой тест стриминга LangGraph без реальных данных и БД. + +**Запуск:** + +```bash +cd backend +$env:PYTHONIOENCODING="utf-8"; ./venv/Scripts/python ../tests/test_simple_graph.py # Windows PowerShell +# или +python ../tests/test_simple_graph.py # Linux/Mac +``` + +**Что тестирует:** +- Различные режимы стриминга (`updates`, `messages`, `values`, `debug`) +- Обработку событий через callback +- Формат событий от LangGraph + +### test_langgraph_events.py + +Полный тест с реальным ReviewerAgent и БД. + +**Требования:** +- Работающая БД с данными +- Существующий Review ID, PR Number, Repository ID +- Настроенный `.env` файл + +**Запуск:** + +1. Отредактируйте параметры в файле: + ```python + TEST_REVIEW_ID = 1 + TEST_PR_NUMBER = 5 + TEST_REPOSITORY_ID = 1 + ``` + +2. Запустите: + ```bash + cd backend + python ../tests/test_langgraph_events.py + ``` + +### test_llm_streaming.py + +Тест стриминга LLM messages с реальным Ollama. + +**Требования:** +- Ollama запущен (`ollama serve`) +- Модель загружена (`ollama pull qwen2.5-coder:3b`) + +**Запуск:** + +```bash +cd backend +$env:PYTHONIOENCODING="utf-8"; ./venv/Scripts/python ../tests/test_llm_streaming.py # Windows +python ../tests/test_llm_streaming.py # Linux/Mac +``` + +## Добавление новых тестов + +Добавляйте новые тесты в эту папку с префиксом `test_`. + +## Полезные ссылки + +- [TEST_STREAMING.md](../docs/TEST_STREAMING.md) - Детальная документация по тестированию стриминга + diff --git a/test_langgraph_events.py b/tests/test_langgraph_events.py similarity index 100% rename from test_langgraph_events.py rename to tests/test_langgraph_events.py diff --git a/tests/test_llm_streaming.py b/tests/test_llm_streaming.py new file mode 100644 index 0000000..1527e92 --- /dev/null +++ b/tests/test_llm_streaming.py @@ -0,0 +1,137 @@ +""" +Тест стриминга LLM messages от LangGraph +""" + +import asyncio +from langgraph.graph import StateGraph, END +from typing import TypedDict, Annotated +import operator +from langchain_ollama import OllamaLLM + + +class TestState(TypedDict): + messages: Annotated[list, operator.add] + result: str + + +async def llm_node(state: TestState) -> TestState: + """Нода с LLM вызовом""" + print(" [LLM NODE] Вызов LLM...") + + llm = OllamaLLM( + model="qwen2.5-coder:3b", + base_url="http://localhost:11434", + temperature=0.7 + ) + + # Простой промпт для быстрого ответа + prompt = "Напиши короткую проверку кода на Python (не более 100 символов)" + + response = await llm.ainvoke(prompt) + + print(f" [LLM NODE] Ответ получен: {response[:50]}...") + + return { + "messages": [{"role": "ai", "content": response}], + "result": response + } + + +def create_test_graph(): + """Создает тестовый граф с LLM""" + workflow = StateGraph(TestState) + + workflow.add_node("llm_call", llm_node) + + workflow.set_entry_point("llm_call") + workflow.add_edge("llm_call", END) + + return workflow.compile() + + +async def test_with_llm(): + """Тест стриминга с LLM""" + print("\n" + "="*80) + print("ТЕСТ СТРИМИНГА LLM MESSAGES") + print("="*80) + + graph = create_test_graph() + + initial_state: TestState = { + "messages": [], + "result": "" + } + + # Тест: updates + messages + print(f"\n🔍 Тест: stream_mode=['updates', 'messages']") + print("-" * 80) + + event_count = 0 + messages_count = 0 + + async for event in graph.astream(initial_state, stream_mode=["updates", "messages"]): + event_count += 1 + + if isinstance(event, tuple) and len(event) >= 2: + event_type, event_data = event[0], event[1] + + print(f"\n📨 Event #{event_count}") + print(f" Type: {event_type}") + print(f" Data type: {type(event_data)}") + + if event_type == 'updates': + print(f" ✅ Node update") + if isinstance(event_data, dict): + for node_name in event_data.keys(): + print(f" Node: {node_name}") + + elif event_type == 'messages': + messages_count += 1 + print(f" 💬 LLM Messages (#{messages_count})") + + if isinstance(event_data, (list, tuple)): + for i, msg in enumerate(event_data): + print(f" Message {i+1}:") + + # Извлекаем контент + if hasattr(msg, 'content'): + content = msg.content + print(f" Content: {content[:100]}...") + elif isinstance(msg, dict): + print(f" Dict: {msg}") + else: + print(f" Type: {type(msg)}") + print(f" Str: {str(msg)[:100]}...") + + print(f"\n" + "="*80) + print(f"✅ Всего событий: {event_count}") + print(f"✅ Messages событий: {messages_count}") + print("="*80) + + +async def main(): + print("\n" + "="*80) + print("ТЕСТИРОВАНИЕ LLM STREAMING В LANGGRAPH") + print("="*80) + print("\nПроверка Ollama...") + + try: + # Проверяем что Ollama доступен + from langchain_ollama import OllamaLLM + test_llm = OllamaLLM(model="qwen2.5-coder:3b", base_url="http://localhost:11434") + result = await test_llm.ainvoke("test") + print("✅ Ollama работает!") + except Exception as e: + print(f"❌ Ошибка подключения к Ollama: {e}") + print("\n⚠️ Убедитесь что Ollama запущен: ollama serve") + print("⚠️ И модель загружена: ollama pull qwen2.5-coder:3b\n") + return + + await test_with_llm() + + print("\n✅ Тестирование завершено\n") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/test_simple_graph.py b/tests/test_simple_graph.py similarity index 100% rename from test_simple_graph.py rename to tests/test_simple_graph.py