feat: Add review events persistence, version display, and auto-versioning system
This commit is contained in:
1 parent
cfba28f913
commit
2db1225618
56 files changed
+750
-436
No files matched your search
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+2
-1
@@ -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__":
|
||||
|
||||
@@ -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);
|
||||
|
||||
Whitespace-only changes.
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 стриминга
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-dark-bg">
|
||||
<div className="min-h-screen bg-dark-bg pb-12">
|
||||
<Navigation />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
@@ -78,6 +79,8 @@ function AppContent() {
|
||||
<Route path="/reviews/:id" element={<ReviewDetail />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ReviewEvent[]>(`/reviews/${reviewId}/events`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getBackendVersion = async () => {
|
||||
const response = await api.get<{ version: string }>('/version');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -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 (
|
||||
<footer className="fixed bottom-0 left-0 right-0 bg-dark-card border-t border-dark-border py-2 px-4 z-10">
|
||||
<div className="container mx-auto flex items-center justify-between text-xs text-dark-text-muted">
|
||||
<div>
|
||||
AI Code Review Agent
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span>
|
||||
Backend v{versionData?.version || '...'}
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/yourusername/ai-review-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-dark-text-secondary transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ReviewStreamProps> = ({ 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;
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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) - Детальная документация по тестированию стриминга
|
||||
|
||||
File renamed without changes.
@@ -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())
|
||||
|
||||
File renamed without changes.
Reference in new issue
Block a user