Initial commit: Neural Sync - AI-native project management platform
Features: - Signal monitoring with AI strategic analysis - Experiment management with score tracking - Context knowledge nodes with RAG - Multi-model voting for high-impact signals - Ollama integration for local LLM Tech stack: - Backend: FastAPI + SQLAlchemy + PostgreSQL - Frontend: Vue 3 + Naive UI + TypeScript - AI: Ollama (llama3, nomic-embed-text) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0981acb06c
|
|
@ -0,0 +1,36 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.DS_Store
|
||||
|
||||
# Ollama
|
||||
ollama_bin/
|
||||
.ollama/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
nohup.out
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Neural Sync** is an AI-native project management platform designed for small (10-person) AI/ML technical teams. Unlike traditional PM tools that manage static tasks, this system manages "Living Context" - external signals, AI experiments, and cognitive knowledge nodes with version tracking and semantic embedding.
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
The system operates on three pillars:
|
||||
1. **Signals (Pulse)**: External triggers (market news, model releases, competitor updates) captured from sources like HackerNews
|
||||
2. **Context Nodes (Cognitive Core)**: Atomic knowledge units with vector embeddings for semantic search - when these change, dependent experiments auto-flag as "outdated"
|
||||
3. **Experiments (Lab)**: Evaluation-driven tasks where success is measured by metrics (accuracy, latency) rather than just completion
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Backend
|
||||
- **FastAPI** with async/await patterns
|
||||
- **SQLAlchemy 2.0** async with database-agnostic design (PostgreSQL with pgvector or SQLite fallback)
|
||||
- **Alembic** for database migrations
|
||||
- **Ollama** integration for local LLM operations (llama3, nomic-embed-text models)
|
||||
|
||||
### Frontend
|
||||
- **Vue 3** with Composition API and `<script setup>` syntax
|
||||
- **Naive UI** component library (chosen over Element Plus for data-dense applications)
|
||||
- **TypeScript**
|
||||
- **Vite** 8.x beta
|
||||
- **ECharts** via vue-echarts for radar/competitiveness visualization
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
# Navigate to backend
|
||||
cd backend
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run database migrations
|
||||
alembic upgrade head
|
||||
|
||||
# Seed initial data (development only)
|
||||
python -m seed
|
||||
|
||||
# Start backend server (dev mode with hot reload)
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
# Navigate to frontend
|
||||
cd frontend
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start dev server (accessible on network with --host flag)
|
||||
npm run dev -- --host
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Database Setup
|
||||
|
||||
**PostgreSQL (production)**:
|
||||
```bash
|
||||
# Create database with pgvector extension
|
||||
createdb neural_sync
|
||||
psql neural_sync -c "CREATE EXTENSION vector;"
|
||||
```
|
||||
|
||||
**SQLite (development)**:
|
||||
- Set `DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db` in environment
|
||||
- No external setup required
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend Structure ([`backend/app/`](backend/app/))
|
||||
|
||||
- **[`main.py`](backend/app/main.py)**: FastAPI application with all endpoints
|
||||
- Webhook handlers for Git and Lark events
|
||||
- Signal ingestion with strategic impact analysis
|
||||
- CRUD operations for experiments and context nodes
|
||||
|
||||
- **[`models.py`](backend/app/models.py)**: SQLAlchemy models with compatibility layer
|
||||
- `Signal`: External signals with strategic analysis and generated tasks
|
||||
- `ContextNode`: Knowledge units with vector embeddings and AI-extracted insights
|
||||
- `Experiment`: Hypothesis-driven experiments with score tracking
|
||||
- Note: Vector type and UUID have SQLite fallback for development
|
||||
|
||||
- **[`services/`](backend/app/services/)**: External integrations
|
||||
- **[`llm.py`](backend/app/services/llm.py)**: Ollama client for embeddings, task breakdown, strategic evaluation, code audit
|
||||
- **[`plane.py`](backend/app/services/plane.py)**: Plane PM tool integration (simulation mode without API key)
|
||||
- **[`lark.py`](backend/app/services/lark.py)**: Lark/Feishu bot messaging with rich cards
|
||||
- **[`fetcher.py`](backend/app/services/fetcher.py)**: HackerNews signal fetching
|
||||
- **[`analyzer.py`](backend/app/services/analyzer.py)**: Fallback keyword-based impact scoring
|
||||
|
||||
### Frontend Structure ([`frontend/src/`](frontend/src/))
|
||||
|
||||
- **[`App.vue`](frontend/src/App.vue)**: Main layout with dark theme and horizontal navigation
|
||||
- **[`components/SignalDashboard.vue`](frontend/src/components/SignalDashboard.vue)**: Complex command center with:
|
||||
- Signal aggregation by source
|
||||
- Competitive moat visualization with progress comparison
|
||||
- ROI scorecard and radar charts
|
||||
- Typewriter effect for AI analysis
|
||||
- Polling-based status updates
|
||||
- **[`components/ExperimentBoard.vue`](frontend/src/components/ExperimentBoard.vue)**: Experiment grid with auto-status based on scores
|
||||
- **[`components/ContextManager.vue`](frontend/src/components/ContextManager.vue)**: Knowledge nodes with AI insight display (moat, tech_specs)
|
||||
|
||||
## Key Integration Points
|
||||
|
||||
### Plane Integration
|
||||
- Environment variables: `PLANE_API_URL`, `PLANE_API_KEY`, `PLANE_WORKSPACE_SLUG`, `PLANE_PROJECT_ID`
|
||||
- Without API key, runs in simulation mode (logs to console)
|
||||
- Supports creating issues with labels, priority, and time estimates
|
||||
- Status updates require State UUID lookup (not fully implemented in MVP)
|
||||
|
||||
### Ollama Integration
|
||||
- Default host: `http://127.0.0.1:11434`
|
||||
- Models used: `llama3` (general), `nomic-embed-text` (embeddings)
|
||||
- All LLM calls are synchronous but wrapped in async endpoints
|
||||
- Includes mock fallback responses for "Pixelle-Video" signals
|
||||
|
||||
### Git Webhook Flow
|
||||
1. Commit message contains task ID (e.g., `#PROJ-123`)
|
||||
2. Webhook extracts diff and message
|
||||
3. [`llm.audit_code_change()`](backend/app/services/llm.py) evaluates against task requirement
|
||||
4. If passed: updates Plane status, marks task Done in Signal
|
||||
|
||||
## Database Schema Notes
|
||||
|
||||
### Vector Embeddings
|
||||
- PostgreSQL: Uses `Vector(768)` from pgvector
|
||||
- SQLite: Fallback to JSON storage for development
|
||||
- Embeddings generated via Ollama's `nomic-embed-text` model
|
||||
|
||||
### Migration Files
|
||||
- Located in [`backend/alembic/versions/`](backend/alembic/versions/)
|
||||
- Key migrations: initial tables, pgvector support, signal model updates
|
||||
|
||||
## Development Notes
|
||||
|
||||
- **CORS**: Currently allows all origins (`*`) - restrict for production
|
||||
- **Database echo**: Enabled in development - set `echo=False` in [`database.py`](backend/app/database.py) for production
|
||||
- **Frontend API**: Hardcoded to `http://127.0.0.1:8000` - configure via env var for deployment
|
||||
- **Polling**: Signal dashboard polls every 5 seconds with smart diff detection to minimize re-renders
|
||||
- **AI Analysis**: All LLM prompts are in [`llm.py`](backend/app/services/llm.py) with fallback mock data for specific keywords
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
The system includes a mock fallback for certain signal types (e.g., "Pixelle-Video", "Civitai", "Sora") to demonstrate strategic analysis without requiring a live Ollama instance. These are defined in [`llm.py`](backend/app/services/llm.py) with hardcoded responses.
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# Neural Sync - AI Project Manager
|
||||
|
||||
This is a custom internal AI Project Management platform designed to align cognitive processes and manage AI experiments efficiently. It focuses on speed and model adaptation, providing a centralized "AI Kitchen" for the team.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **Backend**: Python (FastAPI) with PostgreSQL (pgvector) for vector storage.
|
||||
- **Frontend**: Vue 3 + TypeScript + Naive UI (Web-only, Desktop First).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
- PostgreSQL 15+ (running on port 5435 by default)
|
||||
- `pgvector` extension for PostgreSQL
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Database Setup
|
||||
|
||||
Ensure PostgreSQL is running and create the database:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE neural_sync;
|
||||
\c neural_sync
|
||||
CREATE EXTENSION vector;
|
||||
```
|
||||
|
||||
### 2. Backend Setup
|
||||
|
||||
Navigate to the `backend` directory:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
pip install asyncpg alembic pgvector # Install key dependencies if not in requirements.txt
|
||||
```
|
||||
|
||||
Run database migrations:
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Start the backend server:
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### 3. Frontend Setup
|
||||
|
||||
Navigate to the `frontend` directory:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the frontend development server:
|
||||
|
||||
```bash
|
||||
npm run dev -- --host
|
||||
```
|
||||
|
||||
Access the frontend at: `http://localhost:5173`
|
||||
|
||||
## Features
|
||||
|
||||
- **Signal Dashboard**: Monitor external signals and system status.
|
||||
- **Experiment Board**: Manage AI experiments with hypothesis tracking and scoring.
|
||||
- **Context Manager**: Define and manage cognitive nodes (RAG slices, rules).
|
||||
|
||||
## Notes
|
||||
|
||||
- The interface is currently optimized for **Desktop Web** usage only. Mobile support is not a priority.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Database Configuration
|
||||
DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db
|
||||
# For PostgreSQL with pgvector:
|
||||
# DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5435/neural_sync
|
||||
|
||||
# Ollama LLM Service (default: http://127.0.0.1:11434)
|
||||
OLLAMA_HOST=http://127.0.0.1:11434
|
||||
|
||||
# Multi-Model Voting Configuration
|
||||
# For high-impact signals (impact_score >= VOTING_THRESHOLD), multiple models will vote
|
||||
# Available models: llama3, llama3.2, qwen2.5, mistral, deepseek-coder, etc.
|
||||
VOTING_MODELS=llama3,qwen2.5,mistral
|
||||
VOTING_THRESHOLD=0.8
|
||||
|
||||
# Business Context for AI Strategic Analysis
|
||||
# Customize this to match your actual product/team
|
||||
# This context is used by the AI to evaluate external signals
|
||||
BUSINESS_CONTEXT=Pixelle-Video: An AI-powered automated short video generation platform. Core features: topic-to-video automation (script generation → AI images/videos → TTS → BGM → final video). Tech stack: Python, Streamlit, FastAPI, ComfyUI workflows. AI models: FLUX/Qwen/SDXL for images, WAN 2.1/2.2 for videos, Edge TTS/Index TTS for voice. Supports multiple pipelines: standard generation, custom assets, digital human, image-to-video, action transfer.
|
||||
|
||||
# Plane PM Tool Integration (optional, simulation mode if not set)
|
||||
PLANE_API_URL=https://api.plane.so/api/v1
|
||||
PLANE_API_KEY=
|
||||
PLANE_WORKSPACE_SLUG=
|
||||
PLANE_PROJECT_ID=
|
||||
|
||||
# Lark/Feishu Bot Integration (optional)
|
||||
LARK_APP_ID=
|
||||
LARK_APP_SECRET=
|
||||
LARK_VERIFICATION_TOKEN=
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[timezone]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for default
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator"
|
||||
# version_locations = %(here)s/alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5435/neural_sync
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from app.models import Base
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
"""Add tables with pgvector
|
||||
|
||||
Revision ID: 380f4e61b41c
|
||||
Revises:
|
||||
Create Date: 2026-02-18 19:50:33.287777
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import pgvector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '380f4e61b41c'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('context_nodes',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=1536), nullable=True),
|
||||
sa.Column('version', sa.Integer(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('created_by', sa.UUID(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('experiments',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('title', sa.String(length=255), nullable=True),
|
||||
sa.Column('hypothesis', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.String(length=50), nullable=True),
|
||||
sa.Column('owner_id', sa.UUID(), nullable=True),
|
||||
sa.Column('linked_context_nodes', sa.ARRAY(sa.UUID()), nullable=True),
|
||||
sa.Column('target_metric', sa.String(length=50), nullable=True),
|
||||
sa.Column('current_score', sa.Float(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('signals',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('source', sa.String(length=50), nullable=True),
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('impact_score', sa.Float(), nullable=True),
|
||||
sa.Column('processed', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('signals')
|
||||
op.drop_table('experiments')
|
||||
op.drop_table('context_nodes')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"""resize_vector
|
||||
|
||||
Revision ID: d3c63f669b0a
|
||||
Revises: 380f4e61b41c
|
||||
Create Date: 2026-02-18 22:10:28.693146
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import pgvector
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd3c63f669b0a'
|
||||
down_revision: Union[str, Sequence[str], None] = '380f4e61b41c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('context_nodes', 'embedding',
|
||||
existing_type=pgvector.sqlalchemy.vector.VECTOR(dim=1536),
|
||||
type_=pgvector.sqlalchemy.vector.VECTOR(dim=768),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('context_nodes', 'embedding',
|
||||
existing_type=pgvector.sqlalchemy.vector.VECTOR(dim=768),
|
||||
type_=pgvector.sqlalchemy.vector.VECTOR(dim=1536),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"""update_signal_model
|
||||
|
||||
Revision ID: f38d00f6fee4
|
||||
Revises: d3c63f669b0a
|
||||
Create Date: 2026-02-18 23:29:57.999091
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f38d00f6fee4'
|
||||
down_revision: Union[str, Sequence[str], None] = 'd3c63f669b0a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('signals', sa.Column('strategic_analysis', sa.Text(), nullable=True))
|
||||
op.add_column('signals', sa.Column('tasks_generated', sa.Text(), nullable=True))
|
||||
op.add_column('signals', sa.Column('created_at', sa.DateTime(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('signals', 'created_at')
|
||||
op.drop_column('signals', 'tasks_generated')
|
||||
op.drop_column('signals', 'strategic_analysis')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./neural_sync.db")
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
|
|
@ -0,0 +1,960 @@
|
|||
from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
from .database import get_db, engine, Base
|
||||
from .models import ContextNode, Experiment, Signal, SignalFeedback
|
||||
from .services.fetcher import fetch_hn_stories
|
||||
from .services.analyzer import analyze_impact
|
||||
from .services.llm import llm_service
|
||||
|
||||
app = FastAPI(title="Neural Sync API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, specify allowed origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Signal Priority Enum
|
||||
class SignalPriority(str, Enum):
|
||||
CRITICAL = "critical" # impact >= 0.9: Immediate processing + notification
|
||||
HIGH = "high" # 0.7 <= impact < 0.9: Async processing + notification
|
||||
MEDIUM = "medium" # 0.5 <= impact < 0.7: Async processing only
|
||||
LOW = "low" # impact < 0.5: Record only, no processing
|
||||
|
||||
def classify_signal_priority(impact_score: float, confidence: float = 0.0) -> SignalPriority:
|
||||
"""
|
||||
Classify signal priority based on impact score and confidence.
|
||||
|
||||
Priority Matrix:
|
||||
- CRITICAL: impact >= 0.9 (regardless of confidence)
|
||||
- HIGH: 0.7 <= impact < 0.9 AND confidence >= 0.6
|
||||
- MEDIUM: 0.5 <= impact < 0.7 OR (0.7 <= impact < 0.9 AND confidence < 0.6)
|
||||
- LOW: impact < 0.5
|
||||
"""
|
||||
if impact_score >= 0.9:
|
||||
return SignalPriority.CRITICAL
|
||||
elif impact_score >= 0.7 and confidence >= 0.6:
|
||||
return SignalPriority.HIGH
|
||||
elif impact_score >= 0.5 or (impact_score >= 0.7 and confidence < 0.6):
|
||||
return SignalPriority.MEDIUM
|
||||
else:
|
||||
return SignalPriority.LOW
|
||||
|
||||
# Pydantic Models
|
||||
class SignalCreate(BaseModel):
|
||||
source: str
|
||||
content: str
|
||||
impact_score: float
|
||||
priority: SignalPriority = None # Will be auto-calculated if not provided
|
||||
|
||||
class ExperimentCreate(BaseModel):
|
||||
title: str
|
||||
hypothesis: str
|
||||
target_metric: str
|
||||
linked_context_nodes: List[uuid.UUID] = []
|
||||
|
||||
class ExperimentUpdate(BaseModel):
|
||||
current_score: float
|
||||
|
||||
class ContextNodeCreate(BaseModel):
|
||||
content: str
|
||||
|
||||
from .services.plane import plane_service
|
||||
|
||||
class TaskBreakdownRequest(BaseModel):
|
||||
description: str
|
||||
sync_to_plane: bool = False
|
||||
|
||||
class TaskItem(BaseModel):
|
||||
task_name: str
|
||||
type: str
|
||||
estimated_hours: float
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
class TaskBreakdownResponse(BaseModel):
|
||||
tasks: List[TaskItem]
|
||||
sync_result: Optional[dict] = None
|
||||
|
||||
# Endpoints
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
@app.get("/signals/")
|
||||
async def list_signals(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Signal).order_by(Signal.impact_score.desc()))
|
||||
signals = result.scalars().all()
|
||||
return signals
|
||||
|
||||
@app.post("/signals/", response_model=dict)
|
||||
async def create_signal(signal: SignalCreate, db: AsyncSession = Depends(get_db)):
|
||||
new_signal = Signal(
|
||||
source=signal.source,
|
||||
content=signal.content,
|
||||
impact_score=signal.impact_score
|
||||
)
|
||||
db.add(new_signal)
|
||||
await db.commit()
|
||||
await db.refresh(new_signal)
|
||||
return {"id": str(new_signal.id), "status": "received"}
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request
|
||||
|
||||
# ... existing imports ...
|
||||
|
||||
# Pydantic Models for Webhook
|
||||
class WebhookPayload(BaseModel):
|
||||
# This structure depends on the Git provider (GitHub/GitLab)
|
||||
# We'll support a generic structure or try to parse flexibly
|
||||
commits: List[dict] = []
|
||||
repository: dict = {}
|
||||
ref: str = ""
|
||||
|
||||
# ... existing models ...
|
||||
|
||||
from .services.lark import LarkService
|
||||
|
||||
lark_service = LarkService()
|
||||
|
||||
@app.post("/webhooks/git")
|
||||
async def handle_git_webhook(payload: dict, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Receives Git webhooks.
|
||||
1. Extract Task ID and Code Diff.
|
||||
2. AI Code Audit.
|
||||
3. If Passed -> Update Plane & Signal status.
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
|
||||
print(f"Received Webhook Payload: {payload}")
|
||||
|
||||
commits = payload.get("commits", [])
|
||||
if not commits:
|
||||
return {"status": "ignored", "reason": "No commits found"}
|
||||
|
||||
updated_tasks = []
|
||||
|
||||
for commit in commits:
|
||||
message = commit.get("message", "")
|
||||
# Mocking diff retrieval - in real life, fetch from commit.url or diff_url
|
||||
# Here we assume the payload might simulate a diff or we rely on message/added files
|
||||
code_diff = commit.get("patch") or str(commit.get("modified", []))
|
||||
|
||||
# Regex to find Task IDs like #PROJ-123 or #123 or #Task-Name
|
||||
# Matching task names is harder, let's stick to ID or simple name match if ID not found
|
||||
task_matches = re.findall(r'#([A-Za-z0-9-]+-\d+|[A-Za-z0-9]+-\d+)', message)
|
||||
|
||||
for task_id in task_matches:
|
||||
print(f"Processing Task ID: {task_id}")
|
||||
|
||||
# 1. Find the Signal that generated this task (to get task details/context)
|
||||
# We search in the JSON column 'tasks_generated'
|
||||
# Postgres JSON search is powerful but for MVP let's do a simple check or assume we have context.
|
||||
# Actually, to Audit properly, we need the Task Description.
|
||||
# Let's search specifically for this task_id in all signals.
|
||||
|
||||
# Optimized search: Select signals where tasks_generated contains the task_id
|
||||
# For MVP with SQLite/Simple Postgres usage: Fetch recent signals and check in python
|
||||
|
||||
result = await db.execute(select(Signal).order_by(Signal.created_at.desc()).limit(20))
|
||||
signals = result.scalars().all()
|
||||
|
||||
target_task = None
|
||||
target_signal = None
|
||||
|
||||
for sig in signals:
|
||||
if not sig.tasks_generated: continue
|
||||
try:
|
||||
tasks = json.loads(sig.tasks_generated)
|
||||
for t in tasks:
|
||||
# Match by Plane ID or Key
|
||||
if t.get("plane_key") == task_id or t.get("plane_id") == task_id:
|
||||
target_task = t
|
||||
target_signal = sig
|
||||
break
|
||||
except:
|
||||
continue
|
||||
if target_task: break
|
||||
|
||||
task_name = target_task.get("task_name") if target_task else f"Task {task_id}"
|
||||
|
||||
# 2. AI Code Audit
|
||||
audit_result = llm_service.audit_code_change(task_name, message, code_diff)
|
||||
|
||||
if audit_result.get("passed"):
|
||||
print(f"✅ Audit Passed for {task_id}")
|
||||
# 3. Update Plane
|
||||
plane_res = await plane_service.update_issue_status(task_id, status="Done")
|
||||
|
||||
# 4. Update Signal DB (Mark task as Done in JSON)
|
||||
if target_signal and target_task:
|
||||
tasks = json.loads(target_signal.tasks_generated)
|
||||
for t in tasks:
|
||||
if t.get("plane_key") == task_id or t.get("plane_id") == task_id:
|
||||
t["status"] = "Done"
|
||||
t["audit_feedback"] = audit_result.get("reason")
|
||||
# Record completion time for metrics
|
||||
if "completed_at" not in t:
|
||||
from datetime import datetime
|
||||
t["completed_at"] = datetime.utcnow().isoformat()
|
||||
|
||||
target_signal.tasks_generated = json.dumps(tasks)
|
||||
await db.commit()
|
||||
|
||||
updated_tasks.append({
|
||||
"task_id": task_id,
|
||||
"status": "Done",
|
||||
"audit": audit_result
|
||||
})
|
||||
else:
|
||||
print(f"❌ Audit Failed for {task_id}: {audit_result.get('reason')}")
|
||||
updated_tasks.append({
|
||||
"task_id": task_id,
|
||||
"status": "Failed",
|
||||
"audit": audit_result
|
||||
})
|
||||
|
||||
return {"status": "processed", "updated_tasks": updated_tasks}
|
||||
|
||||
@app.post("/webhooks/lark")
|
||||
async def handle_lark_webhook(request: Request):
|
||||
"""
|
||||
Receives Lark Bot Webhook events.
|
||||
When a message is received:
|
||||
1. Extract content.
|
||||
2. Call AI Task Breakdown.
|
||||
3. Sync to Plane.
|
||||
4. Reply with a Card Message.
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return {"status": "error", "message": "Invalid JSON"}
|
||||
|
||||
# Handle Challenge (Lark verification)
|
||||
if "challenge" in data:
|
||||
return {"challenge": data["challenge"]}
|
||||
|
||||
event = data.get("event", {})
|
||||
if not event:
|
||||
return {"status": "ignored"}
|
||||
|
||||
# Check if it's a message event
|
||||
if event.get("type") == "message" or event.get("msg_type") == "text":
|
||||
message_content = event.get("text_without_at_bot") or event.get("content", "")
|
||||
# Parse JSON string content if needed, Lark sends JSON string for 'content'
|
||||
if isinstance(message_content, str) and message_content.startswith("{"):
|
||||
import json
|
||||
try:
|
||||
content_obj = json.loads(message_content)
|
||||
message_content = content_obj.get("text", "")
|
||||
except:
|
||||
pass
|
||||
|
||||
message_content = message_content.strip()
|
||||
chat_id = event.get("open_chat_id") or event.get("chat_id") # V1 vs V2 events
|
||||
|
||||
if not message_content or not chat_id:
|
||||
return {"status": "ignored", "reason": "No content or chat_id"}
|
||||
|
||||
print(f"Received Lark Message: {message_content} from {chat_id}")
|
||||
|
||||
# Call Task Breakdown Engine
|
||||
try:
|
||||
tasks = llm_service.generate_task_breakdown(message_content)
|
||||
|
||||
# Sync to Plane
|
||||
if tasks:
|
||||
await plane_service.sync_tasks(tasks)
|
||||
|
||||
# Send Card Reply
|
||||
# In simulation, this will just print
|
||||
await lark_service.send_card_message(chat_id, message_content, tasks)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing Lark message: {e}")
|
||||
|
||||
return {"status": "processed"}
|
||||
@app.post("/signals/ingest", response_model=dict)
|
||||
async def ingest_signal(signal: SignalCreate, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Ingests external signals (RSS/Crawler) with priority-based processing.
|
||||
|
||||
Processing Strategy by Priority:
|
||||
- CRITICAL (impact >= 0.9): Immediate processing + notification
|
||||
- HIGH (0.7 <= impact < 0.9): Async processing + notification
|
||||
- MEDIUM (0.5 <= impact < 0.7): Async processing only
|
||||
- LOW (impact < 0.5): Record only, no processing
|
||||
"""
|
||||
# 1. Calculate initial priority
|
||||
initial_priority = signal.priority or classify_signal_priority(signal.impact_score)
|
||||
|
||||
# 2. Save Signal with priority
|
||||
new_signal = Signal(
|
||||
source=signal.source,
|
||||
content=signal.content,
|
||||
impact_score=signal.impact_score,
|
||||
priority=initial_priority.value
|
||||
)
|
||||
db.add(new_signal)
|
||||
await db.commit()
|
||||
await db.refresh(new_signal)
|
||||
|
||||
# 3. Priority-based processing
|
||||
if initial_priority == SignalPriority.LOW:
|
||||
# LOW priority: Record only, no processing
|
||||
print(f"[PRIORITY] LOW priority signal recorded: {new_signal.id}")
|
||||
return {
|
||||
"id": str(new_signal.id),
|
||||
"priority": initial_priority.value,
|
||||
"status": "recorded",
|
||||
"message": "Low priority signal recorded without processing"
|
||||
}
|
||||
|
||||
# For MEDIUM, HIGH, and CRITICAL: process the signal
|
||||
# Define async processing function
|
||||
async def process_signal_internal():
|
||||
# RAG: Find relevant context from knowledge base
|
||||
relevant_context = await llm_service.find_relevant_context(
|
||||
signal_content=signal.content,
|
||||
db_session=db,
|
||||
top_k=3,
|
||||
min_similarity=0.3
|
||||
)
|
||||
|
||||
# Strategic Assessment with RAG context and multi-model voting
|
||||
strategy_result = llm_service.evaluate_with_voting(
|
||||
signal_content=signal.content,
|
||||
relevant_context=relevant_context,
|
||||
impact_score=signal.impact_score
|
||||
)
|
||||
|
||||
is_strategic = strategy_result.get("is_strategic", False)
|
||||
decision_proposal = strategy_result.get("decision_proposal", {})
|
||||
strategy_desc = decision_proposal.get("recommendation", "")
|
||||
tasks = strategy_result.get("tasks", [])
|
||||
confidence = strategy_result.get("confidence", 0.0)
|
||||
|
||||
# Re-calculate priority with confidence
|
||||
final_priority = classify_signal_priority(signal.impact_score, confidence)
|
||||
new_signal.priority = final_priority.value
|
||||
|
||||
# Process based on priority
|
||||
sync_result = None
|
||||
notification_sent = False
|
||||
|
||||
if final_priority in [SignalPriority.CRITICAL, SignalPriority.HIGH]:
|
||||
if is_strategic and tasks:
|
||||
# Sync to Plane
|
||||
print(f"[PRIORITY] {final_priority.value.upper()}: Strategic Signal Detected! Strategy: {strategy_desc}")
|
||||
sync_result = await plane_service.sync_tasks(tasks, is_urgent=(final_priority == SignalPriority.CRITICAL))
|
||||
|
||||
# Lark Strategic Alert (for HIGH and CRITICAL)
|
||||
default_chat_id = "oc_1234567890"
|
||||
await lark_service.send_strategic_alert(default_chat_id, signal.content, decision_proposal, tasks)
|
||||
notification_sent = True
|
||||
|
||||
# Update signal impact score to reflect strategic importance
|
||||
new_signal.impact_score = 1.0
|
||||
|
||||
# Store strategic analysis
|
||||
import json
|
||||
analysis_data = {
|
||||
"decision_proposal": decision_proposal,
|
||||
"competitiveness_radar": strategy_result.get("competitiveness_radar"),
|
||||
"detected_tech_stacks": strategy_result.get("detected_tech_stacks", []),
|
||||
"related_knowledge_nodes": strategy_result.get("related_knowledge_nodes", []),
|
||||
"rag_context_used": len(relevant_context) if relevant_context else 0,
|
||||
"rag_context_sources": [ctx['id'] for ctx in (relevant_context or [])]
|
||||
}
|
||||
|
||||
if "_voting" in strategy_result:
|
||||
voting_info = strategy_result["_voting"]
|
||||
analysis_data["voting"] = {
|
||||
"enabled": voting_info.get("enabled"),
|
||||
"models_used": voting_info.get("models_used"),
|
||||
"strategic_votes": voting_info.get("strategic_votes"),
|
||||
"total_votes": voting_info.get("total_votes"),
|
||||
"unanimous": voting_info.get("unanimous")
|
||||
}
|
||||
|
||||
new_signal.strategic_analysis = json.dumps(analysis_data)
|
||||
new_signal.tasks_generated = json.dumps(tasks)
|
||||
new_signal.processed = True
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_signal)
|
||||
|
||||
return {
|
||||
"id": str(new_signal.id),
|
||||
"priority": final_priority.value,
|
||||
"is_strategic": is_strategic,
|
||||
"confidence": confidence,
|
||||
"decision_proposal": decision_proposal,
|
||||
"rag_context_used": len(relevant_context) if relevant_context else 0,
|
||||
"voting": strategy_result.get("_voting"),
|
||||
"tasks_generated": len(tasks),
|
||||
"sync_result": sync_result,
|
||||
"notification_sent": notification_sent
|
||||
}
|
||||
|
||||
# Process based on priority
|
||||
if initial_priority == SignalPriority.CRITICAL:
|
||||
# CRITICAL: Process immediately
|
||||
result = await process_signal_internal()
|
||||
result["processing_mode"] = "synchronous"
|
||||
return result
|
||||
else:
|
||||
# MEDIUM and HIGH: Process in background
|
||||
print(f"[PRIORITY] {initial_priority.value.upper()}: Signal queued for background processing: {new_signal.id}")
|
||||
background_tasks.add_task(process_signal_internal)
|
||||
return {
|
||||
"id": str(new_signal.id),
|
||||
"priority": initial_priority.value,
|
||||
"status": "queued",
|
||||
"processing_mode": "background",
|
||||
"message": f"{initial_priority.value.upper()} priority signal queued for processing"
|
||||
}
|
||||
|
||||
@app.post("/signals/fetch", response_model=dict)
|
||||
async def trigger_signal_fetch(background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Manually trigger the AI Agent to fetch and analyze signals from the web.
|
||||
This now includes full AI strategic analysis for each signal.
|
||||
"""
|
||||
# 1. Fetch raw stories
|
||||
raw_stories = await fetch_hn_stories(limit=30)
|
||||
|
||||
new_signals_count = 0
|
||||
processed_count = 0
|
||||
|
||||
for story in raw_stories:
|
||||
# 2. Check for duplicates (Simple check by content)
|
||||
result = await db.execute(select(Signal).where(Signal.content == story['content']))
|
||||
if result.scalars().first():
|
||||
print(f"Duplicate signal skipped: {story['content'][:30]}...")
|
||||
continue
|
||||
|
||||
# 3. Analyze Impact (keyword-based)
|
||||
impact = analyze_impact(story['content'])
|
||||
|
||||
# 4. Create signal and trigger full AI analysis
|
||||
if impact > 0.4: # Only save relevant stuff
|
||||
# Use the same logic as /signals/ingest
|
||||
initial_priority = classify_signal_priority(impact)
|
||||
|
||||
new_signal = Signal(
|
||||
source=story['source'],
|
||||
content=story['content'],
|
||||
impact_score=impact,
|
||||
priority=initial_priority.value
|
||||
)
|
||||
db.add(new_signal)
|
||||
await db.commit()
|
||||
await db.refresh(new_signal)
|
||||
new_signals_count += 1
|
||||
|
||||
# Skip AI analysis for LOW priority
|
||||
if initial_priority == SignalPriority.LOW:
|
||||
print(f"[FETCH] LOW priority signal saved without analysis: {new_signal.id}")
|
||||
continue
|
||||
|
||||
# Queue for AI analysis (same logic as ingest_signal)
|
||||
async def process_fetched_signal():
|
||||
# RAG: Find relevant context from knowledge base
|
||||
relevant_context = await llm_service.find_relevant_context(
|
||||
signal_content=story['content'],
|
||||
db_session=db,
|
||||
top_k=3,
|
||||
min_similarity=0.3
|
||||
)
|
||||
|
||||
# Strategic Assessment with RAG context
|
||||
strategy_result = llm_service.evaluate_with_voting(
|
||||
signal_content=story['content'],
|
||||
relevant_context=relevant_context,
|
||||
impact_score=impact
|
||||
)
|
||||
|
||||
is_strategic = strategy_result.get("is_strategic", False)
|
||||
decision_proposal = strategy_result.get("decision_proposal", {})
|
||||
confidence = strategy_result.get("confidence", 0.0)
|
||||
|
||||
# Re-calculate priority with confidence
|
||||
final_priority = classify_signal_priority(impact, confidence)
|
||||
new_signal.priority = final_priority.value
|
||||
|
||||
# Store strategic analysis
|
||||
import json
|
||||
analysis_data = {
|
||||
"decision_proposal": decision_proposal,
|
||||
"competitiveness_radar": strategy_result.get("competitiveness_radar"),
|
||||
"detected_tech_stacks": strategy_result.get("detected_tech_stacks", []),
|
||||
"related_knowledge_nodes": strategy_result.get("related_knowledge_nodes", []),
|
||||
"rag_context_used": len(relevant_context) if relevant_context else 0,
|
||||
"rag_context_sources": [ctx['id'] for ctx in (relevant_context or [])],
|
||||
"confidence": confidence,
|
||||
"is_strategic": is_strategic
|
||||
}
|
||||
|
||||
if "_voting" in strategy_result:
|
||||
voting_info = strategy_result["_voting"]
|
||||
analysis_data["voting"] = {
|
||||
"enabled": voting_info.get("enabled"),
|
||||
"models_used": voting_info.get("models_used"),
|
||||
"strategic_votes": voting_info.get("strategic_votes"),
|
||||
"total_votes": voting_info.get("total_votes"),
|
||||
"unanimous": voting_info.get("unanimous")
|
||||
}
|
||||
|
||||
new_signal.strategic_analysis = json.dumps(analysis_data)
|
||||
tasks = strategy_result.get("tasks", [])
|
||||
new_signal.tasks_generated = json.dumps(tasks)
|
||||
new_signal.processed = True
|
||||
|
||||
await db.commit()
|
||||
nonlocal processed_count
|
||||
processed_count += 1
|
||||
print(f"[FETCH] Signal analyzed: {new_signal.id}, strategic={is_strategic}")
|
||||
|
||||
# Process in background for MEDIUM/HIGH priority
|
||||
if initial_priority in [SignalPriority.MEDIUM, SignalPriority.HIGH]:
|
||||
background_tasks.add_task(process_fetched_signal)
|
||||
else:
|
||||
# CRITICAL: process immediately
|
||||
await process_fetched_signal()
|
||||
|
||||
# Auto-stop Ollama after all analysis is complete to save resources
|
||||
_stop_ollama()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"new_signals_count": new_signals_count,
|
||||
"processed_count": processed_count,
|
||||
"message": f"Fetched {new_signals_count} signals, {processed_count} analyzed"
|
||||
}
|
||||
|
||||
def _stop_ollama():
|
||||
"""Stop Ollama service to save resources."""
|
||||
import subprocess
|
||||
import signal
|
||||
import os
|
||||
|
||||
killed = False
|
||||
|
||||
# Method 1: Try to find and kill ollama serve processes
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "ollama"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
pids = result.stdout.strip().split('\n')
|
||||
for pid in pids:
|
||||
if pid:
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGTERM)
|
||||
killed = True
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[AI] pgrep failed: {e}")
|
||||
|
||||
# Method 2: Fallback to pkill -9 (force kill)
|
||||
if not killed:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pkill", "-9", "-f", "ollama"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
killed = result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f"[AI] pkill failed: {e}")
|
||||
|
||||
if killed:
|
||||
print("[AI] Ollama stopped - analysis complete")
|
||||
|
||||
@app.get("/experiments/")
|
||||
async def list_experiments(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Experiment))
|
||||
experiments = result.scalars().all()
|
||||
return experiments
|
||||
|
||||
@app.post("/experiments/")
|
||||
async def create_experiment(experiment: ExperimentCreate, db: AsyncSession = Depends(get_db)):
|
||||
new_experiment = Experiment(
|
||||
title=experiment.title,
|
||||
hypothesis=experiment.hypothesis,
|
||||
target_metric=experiment.target_metric,
|
||||
linked_context_nodes=experiment.linked_context_nodes,
|
||||
status="running" # Auto-start for now
|
||||
)
|
||||
db.add(new_experiment)
|
||||
await db.commit()
|
||||
await db.refresh(new_experiment)
|
||||
return new_experiment
|
||||
|
||||
@app.patch("/experiments/{experiment_id}")
|
||||
async def update_experiment_score(experiment_id: uuid.UUID, update: ExperimentUpdate, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Experiment).where(Experiment.id == experiment_id))
|
||||
experiment = result.scalars().first()
|
||||
if not experiment:
|
||||
raise HTTPException(status_code=404, detail="Experiment not found")
|
||||
|
||||
experiment.current_score = update.current_score
|
||||
|
||||
# Auto-update status based on score (Simple Logic)
|
||||
# In a real scenario, this threshold might be dynamic or configured per experiment
|
||||
if update.current_score >= 0.9:
|
||||
experiment.status = "success"
|
||||
elif update.current_score < 0.5:
|
||||
experiment.status = "failed"
|
||||
else:
|
||||
experiment.status = "running"
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(experiment)
|
||||
return experiment
|
||||
|
||||
@app.get("/context_nodes/")
|
||||
async def list_context_nodes(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(ContextNode))
|
||||
nodes = result.scalars().all()
|
||||
# Simplified serialization to avoid Vector issues
|
||||
return [{
|
||||
"id": node.id,
|
||||
"content": node.content,
|
||||
"status": node.status,
|
||||
"version": node.version,
|
||||
"has_embedding": node.embedding is not None,
|
||||
"ai_insights": node.ai_insights
|
||||
} for node in nodes]
|
||||
|
||||
@app.post("/context_nodes/")
|
||||
async def create_context_node(node: ContextNodeCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Generate Embedding using Ollama
|
||||
embedding = llm_service.get_embedding(node.content)
|
||||
|
||||
# AI Insight Extraction
|
||||
insights = llm_service.extract_context_insights(node.content)
|
||||
|
||||
new_node = ContextNode(
|
||||
content=node.content,
|
||||
embedding=embedding if embedding else None,
|
||||
ai_insights=insights
|
||||
)
|
||||
db.add(new_node)
|
||||
await db.commit()
|
||||
await db.refresh(new_node)
|
||||
|
||||
# Return a simplified response to avoid serialization issues with Vector type
|
||||
return {
|
||||
"id": new_node.id,
|
||||
"content": new_node.content,
|
||||
"status": new_node.status,
|
||||
"version": new_node.version,
|
||||
"has_embedding": new_node.embedding is not None,
|
||||
"ai_insights": new_node.ai_insights
|
||||
}
|
||||
|
||||
@app.post("/tasks/breakdown", response_model=TaskBreakdownResponse)
|
||||
async def breakdown_tasks(request: TaskBreakdownRequest):
|
||||
"""
|
||||
AI Task Breakdown Engine.
|
||||
Receives a natural language description and returns a list of technical tasks.
|
||||
If sync_to_plane=True, it will create issues in Plane (simulated if no API key).
|
||||
"""
|
||||
tasks = llm_service.generate_task_breakdown(request.description)
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=500, detail="Failed to generate task breakdown")
|
||||
|
||||
sync_result = None
|
||||
if request.sync_to_plane:
|
||||
sync_result = await plane_service.sync_tasks(tasks)
|
||||
|
||||
return {"tasks": tasks, "sync_result": sync_result}
|
||||
|
||||
# Feedback endpoints
|
||||
class SignalFeedbackCreate(BaseModel):
|
||||
signal_id: uuid.UUID
|
||||
was_strategic: bool # User's actual judgment
|
||||
reason: str = "" # Optional explanation
|
||||
user_id: str = "" # Optional user identifier
|
||||
|
||||
class SignalFeedbackResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
signal_id: uuid.UUID
|
||||
was_strategic: bool
|
||||
ai_predicted_strategic: bool
|
||||
ai_confidence: float
|
||||
is_correct: bool # Whether AI prediction matched user feedback
|
||||
|
||||
@app.post("/signals/{signal_id}/feedback", response_model=SignalFeedbackResponse)
|
||||
async def submit_signal_feedback(
|
||||
signal_id: uuid.UUID,
|
||||
feedback: SignalFeedbackCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Submit user feedback on AI strategic analysis.
|
||||
This helps the system learn from corrections.
|
||||
"""
|
||||
# 1. Verify signal exists
|
||||
result = await db.execute(select(Signal).where(Signal.id == signal_id))
|
||||
signal = result.scalar_one_or_none()
|
||||
|
||||
if not signal:
|
||||
raise HTTPException(status_code=404, detail="Signal not found")
|
||||
|
||||
# 2. Extract AI prediction from strategic_analysis
|
||||
ai_predicted_strategic = False
|
||||
ai_confidence = 0.0
|
||||
|
||||
if signal.strategic_analysis:
|
||||
try:
|
||||
import json
|
||||
analysis = json.loads(signal.strategic_analysis)
|
||||
decision_proposal = analysis.get("decision_proposal", {})
|
||||
# Try different ways the prediction might be stored
|
||||
ai_predicted_strategic = analysis.get("is_strategic", signal.impact_score >= 0.9)
|
||||
ai_confidence = analysis.get("confidence", 0.0)
|
||||
except:
|
||||
# Fallback to impact_score
|
||||
ai_predicted_strategic = signal.impact_score >= 0.9
|
||||
|
||||
# 3. Create feedback record
|
||||
new_feedback = SignalFeedback(
|
||||
signal_id=signal_id,
|
||||
was_strategic=feedback.was_strategic,
|
||||
reason=feedback.reason,
|
||||
user_id=feedback.user_id,
|
||||
ai_predicted_strategic=ai_predicted_strategic,
|
||||
ai_confidence=ai_confidence
|
||||
)
|
||||
|
||||
db.add(new_feedback)
|
||||
await db.commit()
|
||||
await db.refresh(new_feedback)
|
||||
|
||||
# 4. Calculate if AI was correct
|
||||
is_correct = ai_predicted_strategic == feedback.was_strategic
|
||||
|
||||
# 5. Log for monitoring
|
||||
print(f"[FEEDBACK] Signal {signal_id}: AI predicted {ai_predicted_strategic}, user says {feedback.was_strategic} - {'✓ Correct' if is_correct else '✗ Wrong'}")
|
||||
if feedback.reason:
|
||||
print(f"[FEEDBACK] Reason: {feedback.reason}")
|
||||
|
||||
return SignalFeedbackResponse(
|
||||
id=new_feedback.id,
|
||||
signal_id=new_feedback.signal_id,
|
||||
was_strategic=new_feedback.was_strategic,
|
||||
ai_predicted_strategic=new_feedback.ai_predicted_strategic,
|
||||
ai_confidence=new_feedback.ai_confidence,
|
||||
is_correct=is_correct
|
||||
)
|
||||
|
||||
@app.get("/signals/{signal_id}/feedbacks", response_model=List[SignalFeedbackResponse])
|
||||
async def get_signal_feedbacks(
|
||||
signal_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all feedback for a specific signal.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(SignalFeedback)
|
||||
.where(SignalFeedback.signal_id == signal_id)
|
||||
.order_by(SignalFeedback.created_at.desc())
|
||||
)
|
||||
feedbacks = result.scalars().all()
|
||||
|
||||
return [
|
||||
SignalFeedbackResponse(
|
||||
id=f.id,
|
||||
signal_id=f.signal_id,
|
||||
was_strategic=f.was_strategic,
|
||||
ai_predicted_strategic=f.ai_predicted_strategic,
|
||||
ai_confidence=f.ai_confidence or 0.0,
|
||||
is_correct=f.ai_predicted_strategic == f.was_strategic
|
||||
)
|
||||
for f in feedbacks
|
||||
]
|
||||
|
||||
@app.get("/feedbacks/stats")
|
||||
async def get_feedback_stats(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Get overall feedback statistics for monitoring AI performance.
|
||||
"""
|
||||
result = await db.execute(select(SignalFeedback))
|
||||
all_feedbacks = result.scalars().all()
|
||||
|
||||
total = len(all_feedbacks)
|
||||
if total == 0:
|
||||
return {
|
||||
"total_feedbacks": 0,
|
||||
"accuracy": 0.0,
|
||||
"true_positives": 0,
|
||||
"false_positives": 0,
|
||||
"true_negatives": 0,
|
||||
"false_negatives": 0
|
||||
}
|
||||
|
||||
correct = sum(1 for f in all_feedbacks if f.ai_predicted_strategic == f.was_strategic)
|
||||
true_positives = sum(1 for f in all_feedbacks if f.ai_predicted_strategic and f.was_strategic)
|
||||
false_positives = sum(1 for f in all_feedbacks if f.ai_predicted_strategic and not f.was_strategic)
|
||||
true_negatives = sum(1 for f in all_feedbacks if not f.ai_predicted_strategic and not f.was_strategic)
|
||||
false_negatives = sum(1 for f in all_feedbacks if not f.ai_predicted_strategic and f.was_strategic)
|
||||
|
||||
return {
|
||||
"total_feedbacks": total,
|
||||
"accuracy": correct / total,
|
||||
"true_positives": true_positives,
|
||||
"false_positives": false_positives,
|
||||
"true_negatives": true_negatives,
|
||||
"false_negatives": false_negatives
|
||||
}
|
||||
|
||||
@app.get("/ai/status")
|
||||
async def get_ai_status():
|
||||
"""
|
||||
Check if Ollama AI service is running and available.
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
try:
|
||||
# Use curl to check Ollama status
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://127.0.0.1:11434/api/tags"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3
|
||||
)
|
||||
|
||||
if result.stdout.strip() == "200":
|
||||
# Try to get model list
|
||||
result2 = subprocess.run(
|
||||
["curl", "-s", "http://127.0.0.1:11434/api/tags"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3
|
||||
)
|
||||
if result2.returncode == 0:
|
||||
data = json.loads(result2.stdout)
|
||||
models = data.get("models", [])
|
||||
model_names = [m.get("name", "unknown") for m in models]
|
||||
return {
|
||||
"status": "ready",
|
||||
"ollama_running": True,
|
||||
"available_models": model_names,
|
||||
"model_count": len(model_names)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[AI STATUS] Error: {e}")
|
||||
|
||||
return {
|
||||
"status": "not_ready",
|
||||
"ollama_running": False,
|
||||
"available_models": [],
|
||||
"model_count": 0,
|
||||
"error": "Ollama service not available. Start with: ollama serve"
|
||||
}
|
||||
|
||||
@app.post("/ai/stop")
|
||||
async def stop_ai_service():
|
||||
"""
|
||||
Manually stop Ollama service to save resources and reduce heat.
|
||||
Tries multiple methods to ensure all Ollama processes are terminated.
|
||||
"""
|
||||
import subprocess
|
||||
killed_any = False
|
||||
errors = []
|
||||
|
||||
# Method 1: Kill ollama serve process (the actual model server)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pkill", "-9", "-f", "ollama serve"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("[AI] Killed 'ollama serve' process")
|
||||
killed_any = True
|
||||
except Exception as e:
|
||||
errors.append(f"pkill ollama serve: {e}")
|
||||
|
||||
# Method 2: Kill Ollama.app main process (macOS)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pkill", "-9", "-f", "Ollama.app"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("[AI] Killed Ollama.app process")
|
||||
killed_any = True
|
||||
except Exception as e:
|
||||
errors.append(f"pkill Ollama.app: {e}")
|
||||
|
||||
# Method 3: Fallback - try killall (macOS/Linux)
|
||||
if not killed_any:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["killall", "-9", "ollama"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("[AI] Killed ollama via killall")
|
||||
killed_any = True
|
||||
except Exception as e:
|
||||
errors.append(f"killall ollama: {e}")
|
||||
|
||||
# Method 4: Last resort - kill by port
|
||||
try:
|
||||
# Find process using port 11434
|
||||
result = subprocess.run(
|
||||
["lsof", "-ti", ":11434"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pid = result.stdout.strip().decode()
|
||||
subprocess.run(["kill", "-9", pid], capture_output=True, timeout=5)
|
||||
print(f"[AI] Killed process {pid} using port 11434")
|
||||
killed_any = True
|
||||
except Exception as e:
|
||||
# Port method is optional, don't record as error
|
||||
pass
|
||||
|
||||
if killed_any:
|
||||
return {
|
||||
"status": "stopped",
|
||||
"message": "Ollama service stopped successfully"
|
||||
}
|
||||
elif errors:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to stop Ollama: {'; '.join(errors)}"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "not_running",
|
||||
"message": "Ollama was not running"
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
from sqlalchemy import Column, Integer, String, Boolean, Float, ForeignKey, Text, DateTime, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from .database import Base
|
||||
|
||||
# Compatibility layer for SQLite
|
||||
try:
|
||||
from sqlalchemy.dialects.postgresql import UUID, ARRAY
|
||||
from pgvector.sqlalchemy import Vector
|
||||
# Check if we are using SQLite
|
||||
import os
|
||||
if "sqlite" in os.getenv("DATABASE_URL", ""):
|
||||
raise ImportError("Using SQLite")
|
||||
except ImportError:
|
||||
from sqlalchemy.types import TypeDecorator, CHAR
|
||||
import json
|
||||
|
||||
class UUID(TypeDecorator):
|
||||
impl = CHAR(36)
|
||||
def __init__(self, as_uuid=True):
|
||||
super().__init__()
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None: return None
|
||||
return str(value)
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None: return None
|
||||
return uuid.UUID(value)
|
||||
|
||||
class Vector(TypeDecorator):
|
||||
impl = Text
|
||||
def __init__(self, dim=None):
|
||||
super().__init__()
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None: return None
|
||||
return json.dumps(value)
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None: return None
|
||||
return json.loads(value)
|
||||
|
||||
# Use JSON for ARRAY in SQLite
|
||||
ARRAY = JSON
|
||||
|
||||
class ContextNode(Base):
|
||||
__tablename__ = "context_nodes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
content = Column(Text, nullable=False)
|
||||
embedding = Column(Vector(768))
|
||||
version = Column(Integer, default=1)
|
||||
status = Column(String(20), default='active')
|
||||
created_by = Column(UUID(as_uuid=True), nullable=True) # Placeholder for user link
|
||||
# New fields for AI Insights
|
||||
ai_insights = Column(JSON, nullable=True) # Stores 'moat', 'tech_specs', etc.
|
||||
|
||||
class Experiment(Base):
|
||||
__tablename__ = "experiments"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title = Column(String(255))
|
||||
hypothesis = Column(Text)
|
||||
status = Column(String(50), default='draft')
|
||||
owner_id = Column(UUID(as_uuid=True), nullable=True) # Placeholder
|
||||
linked_context_nodes = Column(JSON, nullable=True) # Changed from ARRAY to JSON for compatibility
|
||||
target_metric = Column(String(50))
|
||||
current_score = Column(Float, nullable=True)
|
||||
|
||||
class Signal(Base):
|
||||
__tablename__ = "signals"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
source = Column(String(50))
|
||||
content = Column(Text)
|
||||
impact_score = Column(Float)
|
||||
priority = Column(String(20), default='low') # critical, high, medium, low
|
||||
processed = Column(Boolean, default=False)
|
||||
# Store strategic analysis result
|
||||
strategic_analysis = Column(Text, nullable=True) # JSON string
|
||||
tasks_generated = Column(Text, nullable=True) # JSON string
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class SignalFeedback(Base):
|
||||
"""User feedback on AI strategic analysis of signals"""
|
||||
__tablename__ = "signal_feedbacks"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
signal_id = Column(UUID(as_uuid=True), ForeignKey('signals.id'), nullable=False)
|
||||
# User's correction of the AI's judgment
|
||||
was_strategic = Column(Boolean, nullable=False) # User's actual judgment
|
||||
reason = Column(Text, nullable=True) # Why they agree/disagree
|
||||
# Reference to original AI prediction for analysis
|
||||
ai_predicted_strategic = Column(Boolean, nullable=False)
|
||||
ai_confidence = Column(Float, nullable=True)
|
||||
# Optional: user info for tracking
|
||||
user_id = Column(String(100), nullable=True) # Simple identifier (email, name, etc.)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationship to signal
|
||||
signal = relationship("Signal", backref="feedbacks")
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
def analyze_impact(content: str) -> float:
|
||||
"""
|
||||
Simulates an AI Agent analyzing the impact of a signal.
|
||||
In a real system, this would call an LLM (GPT-4/DeepSeek).
|
||||
"""
|
||||
content_lower = content.lower()
|
||||
|
||||
# Heuristic Rule-based Scoring (Simulation)
|
||||
score = 0.3 # Base score
|
||||
|
||||
high_impact_keywords = ["release", "announce", "sota", "benchmark", "breakthrough", "deepseek", "gpt-5"]
|
||||
medium_impact_keywords = ["update", "feature", "paper", "research", "tool"]
|
||||
|
||||
for k in high_impact_keywords:
|
||||
if k in content_lower:
|
||||
score += 0.3
|
||||
|
||||
for k in medium_impact_keywords:
|
||||
if k in content_lower:
|
||||
score += 0.1
|
||||
|
||||
# Cap at 0.99
|
||||
return min(score, 0.99)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import httpx
|
||||
import asyncio
|
||||
import random
|
||||
from typing import List, Dict, Any
|
||||
|
||||
HN_TOP_STORIES_URL = "https://hacker-news.firebaseio.com/v0/topstories.json"
|
||||
HN_ITEM_URL = "https://hacker-news.firebaseio.com/v0/item/{item_id}.json"
|
||||
|
||||
KEYWORDS = ["AI", "LLM", "GPT", "DeepSeek", "Transformer", "RAG", "Neural", "Machine Learning", "OpenAI", "Anthropic", "Google", "Meta", "Python", "Data", "Model", "Code", "Tech", "Show HN", "Launch"]
|
||||
|
||||
async def fetch_hn_stories(limit: int = 60) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetches top stories from HackerNews and filters them by AI-related keywords.
|
||||
"""
|
||||
async with httpx.AsyncClient(verify=False, timeout=10.0) as client:
|
||||
try:
|
||||
print(f"Fetching top stories from HN...")
|
||||
response = await client.get(HN_TOP_STORIES_URL)
|
||||
if response.status_code != 200:
|
||||
print(f"Failed to fetch HN stories: {response.status_code}")
|
||||
return []
|
||||
|
||||
story_ids = response.json()[:limit]
|
||||
stories = []
|
||||
|
||||
# Fetch details in batches to avoid overwhelming connection pool
|
||||
batch_size = 10
|
||||
for i in range(0, len(story_ids), batch_size):
|
||||
batch_ids = story_ids[i:i + batch_size]
|
||||
tasks = [client.get(HN_ITEM_URL.format(item_id=sid)) for sid in batch_ids]
|
||||
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for resp in responses:
|
||||
if isinstance(resp, httpx.Response) and resp.status_code == 200:
|
||||
story = resp.json()
|
||||
if story and 'title' in story:
|
||||
title = story['title']
|
||||
# Relaxed Filtering: Match ANY keyword
|
||||
if any(k.lower() in title.lower() for k in KEYWORDS):
|
||||
stories.append({
|
||||
"source": "HackerNews",
|
||||
"content": f"{title} ({story.get('url', 'No URL')})",
|
||||
"raw_data": story
|
||||
})
|
||||
|
||||
print(f"Found {len(stories)} relevant stories.")
|
||||
|
||||
# FORCE DEMO MODE: If we found nothing (or very few), inject a fake signal to ensure user feedback
|
||||
if len(stories) == 0:
|
||||
stories.append({
|
||||
"source": "AI Scout (Simulation)",
|
||||
"content": f"DeepSeek-V4 Preview just leaked on Twitter! Context length: 10M tokens. [Simulated ID: {random.randint(1000,9999)}]",
|
||||
"raw_data": {}
|
||||
})
|
||||
|
||||
return stories
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching HN stories: {e}")
|
||||
# Fallback error signal
|
||||
return [{
|
||||
"source": "System Error",
|
||||
"content": f"Failed to connect to signal source. Error: {str(e)}",
|
||||
"raw_data": {}
|
||||
}]
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import httpx
|
||||
import os
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class LarkService:
|
||||
def __init__(self):
|
||||
self.app_id = os.getenv("LARK_APP_ID", "")
|
||||
self.app_secret = os.getenv("LARK_APP_SECRET", "")
|
||||
self.verification_token = os.getenv("LARK_VERIFICATION_TOKEN", "")
|
||||
self.tenant_access_token = ""
|
||||
|
||||
async def get_tenant_access_token(self):
|
||||
"""
|
||||
Get tenant_access_token from Lark Open Platform
|
||||
"""
|
||||
if not self.app_id or not self.app_secret:
|
||||
print("[LARK] Missing App ID or Secret. Simulation mode.")
|
||||
return "mock_token"
|
||||
|
||||
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"app_secret": self.app_secret
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
data = response.json()
|
||||
if data.get("code") == 0:
|
||||
self.tenant_access_token = data.get("tenant_access_token")
|
||||
return self.tenant_access_token
|
||||
else:
|
||||
print(f"[LARK] Failed to get token: {data}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[LARK] Token request failed: {e}")
|
||||
return None
|
||||
|
||||
async def send_card_message(self, chat_id: str, content: str, tasks: List[Dict[str, Any]]):
|
||||
"""
|
||||
Send a card message to Lark with task breakdown
|
||||
"""
|
||||
token = await self.get_tenant_access_token()
|
||||
if not token:
|
||||
print("[LARK] Cannot send message without token.")
|
||||
return
|
||||
|
||||
url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
# Construct Card Content
|
||||
task_elements = []
|
||||
for task in tasks:
|
||||
task_elements.append({
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**{task.get('task_name')}**\nType: {task.get('type')} | Est: {task.get('estimated_hours')}h"
|
||||
}
|
||||
})
|
||||
task_elements.append({
|
||||
"tag": "hr"
|
||||
})
|
||||
|
||||
card_content = {
|
||||
"config": {
|
||||
"wide_screen_mode": True
|
||||
},
|
||||
"header": {
|
||||
"template": "blue",
|
||||
"title": {
|
||||
"content": "🚀 任务拆解完成 (Synced to Plane)",
|
||||
"tag": "plain_text"
|
||||
}
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"收到需求:**{content}**\n已为您拆解为以下任务并同步至项目看板:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "hr"
|
||||
},
|
||||
*task_elements,
|
||||
{
|
||||
"tag": "note",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "plain_text",
|
||||
"content": f"共 {len(tasks)} 个子任务"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
payload = {
|
||||
"receive_id": chat_id,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card_content)
|
||||
}
|
||||
|
||||
if token == "mock_token":
|
||||
print(f"[LARK SIMULATION] Sending Card to {chat_id}: {json.dumps(card_content, indent=2, ensure_ascii=False)}")
|
||||
return
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
print(f"[LARK] Send message response: {response.json()}")
|
||||
except Exception as e:
|
||||
print(f"[LARK] Send message failed: {e}")
|
||||
|
||||
async def send_strategic_alert(self, chat_id: str, signal_content: str, decision: Dict[str, Any], tasks: List[Dict[str, Any]]):
|
||||
"""
|
||||
Send a strategic alert card message to Lark.
|
||||
"""
|
||||
token = await self.get_tenant_access_token()
|
||||
if not token:
|
||||
print("[LARK] Cannot send message without token.")
|
||||
return
|
||||
|
||||
url = f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
# Construct Card Content
|
||||
# Color: Red for Strategic
|
||||
|
||||
card_content = {
|
||||
"config": {
|
||||
"wide_screen_mode": True
|
||||
},
|
||||
"header": {
|
||||
"template": "red",
|
||||
"title": {
|
||||
"content": "🚨 战略级信号预警 (Strategic Alert)",
|
||||
"tag": "plain_text"
|
||||
}
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**信号摘要**:\n{signal_content}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "hr"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**🤖 AI 决策建议**:\n{decision.get('recommendation', '无建议')}\n\n**预期收益**: {decision.get('expected_benefits', '未评估')}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "hr"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**已自动创建 P0 级任务 ({len(tasks)}个)**:\n" + "\n".join([f"- {t.get('task_name')} ({t.get('estimated_hours')}h)" for t in tasks[:3]]) + (f"\n...等 {len(tasks)-3} 个任务" if len(tasks) > 3 else "")
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "👀 查看指挥部仪表盘"
|
||||
},
|
||||
"type": "primary",
|
||||
"url": "http://localhost:5173/" # Replace with actual deployed URL
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
payload = {
|
||||
"receive_id": chat_id,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card_content)
|
||||
}
|
||||
|
||||
if token == "mock_token":
|
||||
print(f"[LARK SIMULATION] Sending Strategic Alert to {chat_id}: {json.dumps(card_content, indent=2, ensure_ascii=False)}")
|
||||
return
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
print(f"[LARK] Send alert response: {response.json()}")
|
||||
except Exception as e:
|
||||
print(f"[LARK] Send alert failed: {e}")
|
||||
|
||||
lark_service = LarkService()
|
||||
|
|
@ -0,0 +1,685 @@
|
|||
import httpx
|
||||
from typing import List, Dict, Optional
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
class LLMService:
|
||||
def __init__(self, host: str = "http://127.0.0.1:11434"):
|
||||
self.host = host.rstrip('/')
|
||||
# Use trust_env=False to bypass potential proxy issues in the environment
|
||||
self.client = httpx.Client(base_url=self.host, timeout=300.0, trust_env=False)
|
||||
# Business context for strategic analysis
|
||||
self.business_context = os.getenv(
|
||||
"BUSINESS_CONTEXT",
|
||||
"Pixelle-Video: An AI-powered automated short video generation platform. "
|
||||
"Core features: topic-to-video automation (script generation → AI images/videos → TTS → BGM → final video). "
|
||||
"Tech stack: Python, Streamlit, FastAPI, ComfyUI workflows. "
|
||||
"AI models: FLUX/Qwen/SDXL for images, WAN 2.1/2.2 for videos, Edge TTS/Index TTS for voice. "
|
||||
"Supports multiple pipelines: standard generation, custom assets, digital human, image-to-video, action transfer."
|
||||
)
|
||||
# Multi-model voting configuration
|
||||
self.voting_models = os.getenv(
|
||||
"VOTING_MODELS",
|
||||
"llama3,qwen2.5,mistral"
|
||||
).split(",")
|
||||
# Enable voting for high-impact signals (threshold)
|
||||
self.voting_threshold = float(os.getenv("VOTING_THRESHOLD", "0.8"))
|
||||
|
||||
def generate_impact_analysis(self, signal_content: str) -> float:
|
||||
"""
|
||||
Uses Ollama (DeepSeek/Llama3) to analyze the impact of a signal.
|
||||
Returns a score between 0.0 and 1.0.
|
||||
"""
|
||||
prompt = f"""
|
||||
Analyze the following news/signal for an AI engineering team.
|
||||
Determine its "Impact Score" (0.0 to 1.0) based on how critical it is for our RAG/LLM development.
|
||||
|
||||
Signal: "{signal_content}"
|
||||
|
||||
Rules:
|
||||
- 0.9-1.0: Major SOTA model release (e.g., GPT-5, DeepSeek-V4), critical vulnerability, or massive breakthrough.
|
||||
- 0.7-0.8: Important update, new useful library, or relevant research paper.
|
||||
- 0.4-0.6: Interesting but not critical news.
|
||||
- 0.0-0.3: Irrelevant noise.
|
||||
|
||||
Output ONLY the numeric score (e.g., 0.85). Do not output any text.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Try using a lightweight model first for speed
|
||||
response = self.client.post("/api/generate", json={
|
||||
"model": "llama3",
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"LLM Error: {response.status_code} {response.text}")
|
||||
return 0.5
|
||||
|
||||
data = response.json()
|
||||
score_text = data.get('response', '').strip()
|
||||
|
||||
# Extract float from response
|
||||
match = re.search(r"0\.\d+|1\.0", score_text)
|
||||
if match:
|
||||
return float(match.group())
|
||||
return 0.5 # Default if parsing fails
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM Analysis Failed: {e}")
|
||||
return 0.5 # Fallback score
|
||||
|
||||
def get_embedding(self, text: str) -> List[float]:
|
||||
"""
|
||||
Generates vector embedding for text using Ollama.
|
||||
"""
|
||||
try:
|
||||
response = self.client.post("/api/embeddings", json={
|
||||
"model": "nomic-embed-text",
|
||||
"prompt": text
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Embedding Error: {response.status_code} {response.text}")
|
||||
return []
|
||||
|
||||
return response.json().get('embedding', [])
|
||||
except Exception as e:
|
||||
print(f"Embedding Generation Failed: {e}")
|
||||
return []
|
||||
|
||||
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
|
||||
"""
|
||||
Calculate cosine similarity between two vectors.
|
||||
Returns a value between -1 and 1, where 1 means identical.
|
||||
"""
|
||||
if not vec1 or not vec2 or len(vec1) != len(vec2):
|
||||
return 0.0
|
||||
|
||||
import math
|
||||
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
||||
magnitude1 = math.sqrt(sum(a * a for a in vec1))
|
||||
magnitude2 = math.sqrt(sum(b * b for b in vec2))
|
||||
|
||||
if magnitude1 == 0 or magnitude2 == 0:
|
||||
return 0.0
|
||||
|
||||
return dot_product / (magnitude1 * magnitude2)
|
||||
|
||||
async def find_relevant_context(
|
||||
self,
|
||||
signal_content: str,
|
||||
db_session,
|
||||
top_k: int = 3,
|
||||
min_similarity: float = 0.3
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Find relevant ContextNodes using vector similarity search.
|
||||
|
||||
Args:
|
||||
signal_content: The signal text to find context for
|
||||
db_session: SQLAlchemy async session
|
||||
top_k: Maximum number of context nodes to return
|
||||
min_similarity: Minimum similarity threshold (0-1)
|
||||
|
||||
Returns:
|
||||
List of relevant context nodes with their similarity scores
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from ..models import ContextNode
|
||||
|
||||
# 1. Generate embedding for the signal
|
||||
signal_embedding = self.get_embedding(signal_content)
|
||||
if not signal_embedding:
|
||||
print("[RAG] Failed to generate embedding for signal, skipping context retrieval")
|
||||
return []
|
||||
|
||||
# 2. Fetch all active context nodes with embeddings
|
||||
result = await db_session.execute(
|
||||
select(ContextNode)
|
||||
.where(ContextNode.status == 'active')
|
||||
.where(ContextNode.embedding.isnot(None))
|
||||
)
|
||||
nodes = result.scalars().all()
|
||||
|
||||
if not nodes:
|
||||
print("[RAG] No active context nodes with embeddings found")
|
||||
return []
|
||||
|
||||
# 3. Calculate similarity for each node
|
||||
node_similarities = []
|
||||
for node in nodes:
|
||||
# Convert Vector type to list if needed
|
||||
if isinstance(node.embedding, str):
|
||||
import json
|
||||
node_vec = json.loads(node.embedding)
|
||||
else:
|
||||
node_vec = node.embedding
|
||||
|
||||
similarity = self._cosine_similarity(signal_embedding, node_vec)
|
||||
|
||||
if similarity >= min_similarity:
|
||||
node_similarities.append({
|
||||
'id': str(node.id),
|
||||
'content': node.content,
|
||||
'similarity': similarity,
|
||||
'version': node.version,
|
||||
'ai_insights': node.ai_insights
|
||||
})
|
||||
|
||||
# 4. Sort by similarity and return top_k
|
||||
node_similarities.sort(key=lambda x: x['similarity'], reverse=True)
|
||||
top_nodes = node_similarities[:top_k]
|
||||
|
||||
print(f"[RAG] Found {len(top_nodes)} relevant context nodes for signal")
|
||||
for i, node in enumerate(top_nodes):
|
||||
print(f"[RAG] {i+1}. [{node['similarity']:.2f}] {node['content'][:50]}...")
|
||||
|
||||
return top_nodes
|
||||
|
||||
def generate_task_breakdown(self, description: str) -> List[dict]:
|
||||
"""
|
||||
Uses Ollama to break down a natural language description into technical tasks.
|
||||
Returns a list of task dictionaries.
|
||||
"""
|
||||
prompt = f"""
|
||||
You are an expert AI Technical Project Manager.
|
||||
Your goal is to break down a software feature request into specific, actionable technical tasks.
|
||||
|
||||
Feature Request: "{description}"
|
||||
|
||||
Required Output Format: JSON Array only.
|
||||
Each item must have:
|
||||
- "task_name": Concise technical task description.
|
||||
- "type": One of ["frontend", "backend", "database"].
|
||||
- "estimated_hours": Realistic hourly estimate (float).
|
||||
|
||||
Example Output:
|
||||
[
|
||||
{{"task_name": "Design Login Schema", "type": "database", "estimated_hours": 2.0}},
|
||||
{{"task_name": "Implement Auth API", "type": "backend", "estimated_hours": 4.0}}
|
||||
]
|
||||
|
||||
Ensure the breakdown is comprehensive (Frontend, Backend, Database) and strictly follows the JSON format.
|
||||
Do not include any markdown formatting or explanation outside the JSON.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Llama3 supports "format": "json", but sometimes it's picky.
|
||||
# Removing "format": "json" might be safer if the model isn't strictly following it,
|
||||
# but let's try to keep it and improve error handling.
|
||||
response = self.client.post("/api/generate", json={
|
||||
"model": "llama3",
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"LLM Error: {response.status_code} {response.text}")
|
||||
return []
|
||||
|
||||
data = response.json()
|
||||
response_text = data.get('response', '').strip()
|
||||
print(f"DEBUG LLM Response: {response_text}") # Add debug print
|
||||
|
||||
# Clean up potential markdown code blocks
|
||||
if response_text.startswith("```json"):
|
||||
response_text = response_text[7:]
|
||||
if response_text.startswith("```"):
|
||||
response_text = response_text[3:]
|
||||
if response_text.endswith("```"):
|
||||
response_text = response_text[:-3]
|
||||
|
||||
response_text = response_text.strip()
|
||||
|
||||
import json
|
||||
tasks_data = json.loads(response_text)
|
||||
|
||||
# Handle both list and object with 'tasks' key
|
||||
tasks = []
|
||||
if isinstance(tasks_data, list):
|
||||
tasks = tasks_data
|
||||
elif isinstance(tasks_data, dict) and "tasks" in tasks_data:
|
||||
tasks = tasks_data["tasks"]
|
||||
|
||||
# Validate structure
|
||||
validated_tasks = []
|
||||
if isinstance(tasks, list):
|
||||
for t in tasks:
|
||||
if isinstance(t, dict) and "task_name" in t and "type" in t and "estimated_hours" in t:
|
||||
validated_tasks.append(t)
|
||||
|
||||
return validated_tasks
|
||||
|
||||
except Exception as e:
|
||||
print(f"Task Breakdown Failed: {e}")
|
||||
return []
|
||||
|
||||
def _build_strategic_prompt(self, signal_content: str, relevant_context: List[dict] = None) -> str:
|
||||
"""
|
||||
Build the strategic analysis prompt with context.
|
||||
"""
|
||||
# Build RAG context section if available
|
||||
rag_context = ""
|
||||
if relevant_context:
|
||||
rag_context = "\n=== Related Knowledge from Our Database ===\n"
|
||||
rag_context += "The following relevant information was found in our knowledge base:\n\n"
|
||||
for i, ctx in enumerate(relevant_context, 1):
|
||||
rag_context += f"[Relevance: {ctx['similarity']:.1%}]\n{ctx['content']}\n\n"
|
||||
# Include AI insights if available
|
||||
if ctx.get('ai_insights'):
|
||||
insights = ctx['ai_insights']
|
||||
if isinstance(insights, dict):
|
||||
if insights.get('moat'):
|
||||
rag_context += f" → Core Moat: {insights['moat']}\n"
|
||||
if insights.get('tech_specs'):
|
||||
rag_context += f" → Tech Specs: {insights['tech_specs']}\n"
|
||||
rag_context += "\n"
|
||||
rag_context += "Consider this context when evaluating the signal.\n"
|
||||
|
||||
return f"""
|
||||
You are a Chief Technology Officer (CTO) AI Assistant for our team.
|
||||
|
||||
=== Our Product/Team ===
|
||||
{self.business_context}
|
||||
{rag_context}
|
||||
=== External Technology Signal ===
|
||||
Signal: "{signal_content}"
|
||||
|
||||
=== Your Task ===
|
||||
1. Analyze: How does this signal impact OUR product's competitiveness?
|
||||
2. Decide: Is it necessary to follow up immediately? (is_strategic)
|
||||
3. Proposal: If YES, generate a "Strategic Decision Proposal".
|
||||
4. Competitiveness Radar: Evaluate current capabilities vs potential capabilities after adopting this technology.
|
||||
- Dimensions: "Efficiency" (Speed), "Cost" (Savings), "UX" (Experience), "Innovation" (Tech Edge), "Market" (Share).
|
||||
- Score 1-10.
|
||||
5. Detected Tech Stacks: Identify technologies mentioned in the signal (frameworks, models, APIs, tools).
|
||||
- Each stack should have: name, category (framework/model/tool/api), relevance (high/medium/low).
|
||||
6. Related Knowledge Nodes: Suggest relevant knowledge domains for context tracking.
|
||||
- Examples: "Civitai LoRA Integration", "Sora API Adoption", "Video Generation Pipeline".
|
||||
7. Plan: Break down the strategy into specific technical tasks.
|
||||
|
||||
Required Output Format (JSON Only):
|
||||
{{
|
||||
"is_strategic": boolean,
|
||||
"confidence": float,
|
||||
"decision_proposal": {{
|
||||
"situation_analysis": "...",
|
||||
"recommendation": "...",
|
||||
"expected_benefits": "..."
|
||||
}},
|
||||
"competitiveness_radar": {{
|
||||
"current": [5, 4, 6, 5, 5],
|
||||
"projected": [8, 7, 9, 9, 7]
|
||||
}},
|
||||
"detected_tech_stacks": [
|
||||
{{"name": "Technology Name", "category": "framework|model|tool|api", "relevance": "high|medium|low"}}
|
||||
],
|
||||
"related_knowledge_nodes": [
|
||||
{{"title": "Knowledge Node Title", "category": "tech_stack|competitor|market", "summary": "Brief summary"}}
|
||||
],
|
||||
"tasks": [
|
||||
{{
|
||||
"task_name": "Task description",
|
||||
"type": "backend/frontend/database",
|
||||
"estimated_hours": float
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
If is_strategic is false, tasks should be empty, but decision_proposal is still required to explain why.
|
||||
Do not output any markdown formatting or text outside the JSON.
|
||||
"""
|
||||
|
||||
def _evaluate_with_model(self, model: str, prompt: str) -> dict:
|
||||
"""
|
||||
Evaluate strategic impact using a specific model.
|
||||
|
||||
Returns a dict with the analysis result, or an error dict.
|
||||
"""
|
||||
try:
|
||||
response = self.client.post("/api/generate", json={
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"model": model,
|
||||
"error": f"HTTP {response.status_code}",
|
||||
"is_strategic": False,
|
||||
"confidence": 0.0,
|
||||
"detected_tech_stacks": [],
|
||||
"related_knowledge_nodes": []
|
||||
}
|
||||
|
||||
response_text = response.json().get('response', '').strip()
|
||||
result = json.loads(response_text)
|
||||
result["model"] = model
|
||||
# Ensure required fields are present with defaults
|
||||
result.setdefault("detected_tech_stacks", [])
|
||||
result.setdefault("related_knowledge_nodes", [])
|
||||
result.setdefault("tasks", [])
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"model": model,
|
||||
"error": str(e),
|
||||
"is_strategic": False,
|
||||
"confidence": 0.0,
|
||||
"detected_tech_stacks": [],
|
||||
"related_knowledge_nodes": []
|
||||
}
|
||||
|
||||
def evaluate_with_voting(
|
||||
self,
|
||||
signal_content: str,
|
||||
relevant_context: List[dict] = None,
|
||||
impact_score: float = 0.0
|
||||
) -> dict:
|
||||
"""
|
||||
Evaluate strategic impact with multi-model voting for high-impact signals.
|
||||
|
||||
For signals with impact_score >= voting_threshold, uses multiple models
|
||||
and aggregates results using majority voting for is_strategic decision.
|
||||
|
||||
Args:
|
||||
signal_content: The external signal to evaluate
|
||||
relevant_context: Optional list of relevant ContextNodes
|
||||
impact_score: The initial impact score (0.0 to 1.0)
|
||||
|
||||
Returns:
|
||||
A dict with aggregated results including voting metadata
|
||||
"""
|
||||
# MOCK FALLBACK for Pixelle-Video
|
||||
if "Pixelle-Video" in signal_content:
|
||||
return {
|
||||
"is_strategic": True,
|
||||
"confidence": 0.95,
|
||||
"decision_proposal": {
|
||||
"situation_analysis": "Pixelle-Video is a core strategic asset for AI video generation. It aligns with our goal to dominate the automated content creation market.",
|
||||
"recommendation": "Immediate integration and optimization of Pixelle-Video engine.",
|
||||
"expected_benefits": "End-to-end video automation, reduced production costs by 90%, and competitive advantage in short video market."
|
||||
},
|
||||
"competitiveness_radar": {
|
||||
"current": [4, 3, 5, 4, 3],
|
||||
"projected": [9, 8, 8, 9, 8]
|
||||
},
|
||||
"detected_tech_stacks": [
|
||||
{"name": "FLUX", "category": "model", "relevance": "high"},
|
||||
{"name": "ComfyUI", "category": "framework", "relevance": "high"},
|
||||
{"name": "WAN 2.1", "category": "model", "relevance": "medium"},
|
||||
{"name": "Edge TTS", "category": "api", "relevance": "medium"},
|
||||
{"name": "FastAPI", "category": "framework", "relevance": "low"}
|
||||
],
|
||||
"related_knowledge_nodes": [
|
||||
{"title": "Pixelle-Video Pipeline Architecture", "category": "tech_stack", "summary": "Core video generation workflow with script-to-video automation"},
|
||||
{"title": "Competitor Video Generation Tools", "category": "competitor", "summary": "Analysis of Sora, Runway, and Pika Labs capabilities"},
|
||||
{"title": "Short Video Market Trends", "category": "market", "summary": "Growing demand for AI-automated content creation"}
|
||||
],
|
||||
"tasks": [
|
||||
{"task_name": "Integrate Pixelle-Video API into Main Platform", "type": "backend", "estimated_hours": 8.0},
|
||||
{"task_name": "Design Video Generation UI in Frontend", "type": "frontend", "estimated_hours": 12.0},
|
||||
{"task_name": "Optimize Video Rendering Pipeline", "type": "backend", "estimated_hours": 16.0},
|
||||
{"task_name": "Implement User Asset Management for Video", "type": "database", "estimated_hours": 4.0}
|
||||
]
|
||||
}
|
||||
|
||||
prompt = self._build_strategic_prompt(signal_content, relevant_context)
|
||||
|
||||
# Low impact signals: use default model only
|
||||
if impact_score < self.voting_threshold:
|
||||
return self._evaluate_with_model("llama3", prompt)
|
||||
|
||||
# High impact signals: use multi-model voting
|
||||
print(f"[VOTING] High-impact signal detected (score: {impact_score:.2f}), using {len(self.voting_models)} models for voting")
|
||||
|
||||
# Run evaluations in parallel
|
||||
results = []
|
||||
with ThreadPoolExecutor(max_workers=min(3, len(self.voting_models))) as executor:
|
||||
futures = {
|
||||
executor.submit(self._evaluate_with_model, model, prompt): model
|
||||
for model in self.voting_models
|
||||
}
|
||||
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
model = result.get("model", "unknown")
|
||||
is_strategic = result.get("is_strategic", False)
|
||||
confidence = result.get("confidence", 0.0)
|
||||
print(f"[VOTING] {model}: is_strategic={is_strategic}, confidence={confidence:.2f}")
|
||||
|
||||
# Aggregate results using majority voting
|
||||
strategic_votes = sum(1 for r in results if r.get("is_strategic", False))
|
||||
total_votes = len(results)
|
||||
is_strategic_consensus = strategic_votes > total_votes / 2
|
||||
|
||||
# Use the result from the first model that agreed with consensus
|
||||
# for detailed fields (decision_proposal, tasks, etc.)
|
||||
primary_result = None
|
||||
for r in results:
|
||||
if r.get("is_strategic", False) == is_strategic_consensus:
|
||||
primary_result = r
|
||||
break
|
||||
|
||||
# Fallback to first result if no consensus match
|
||||
if primary_result is None:
|
||||
primary_result = results[0]
|
||||
|
||||
# Calculate aggregate confidence
|
||||
valid_confidences = [r.get("confidence", 0.0) for r in results if not r.get("error")]
|
||||
avg_confidence = sum(valid_confidences) / len(valid_confidences) if valid_confidences else 0.0
|
||||
|
||||
# Build final result with voting metadata
|
||||
final_result = {
|
||||
"is_strategic": is_strategic_consensus,
|
||||
"confidence": min(avg_confidence + 0.1, 1.0), # Boost confidence from consensus
|
||||
"decision_proposal": primary_result.get("decision_proposal", {}),
|
||||
"competitiveness_radar": primary_result.get("competitiveness_radar"),
|
||||
"detected_tech_stacks": primary_result.get("detected_tech_stacks", []),
|
||||
"related_knowledge_nodes": primary_result.get("related_knowledge_nodes", []),
|
||||
"tasks": primary_result.get("tasks", []),
|
||||
"_voting": {
|
||||
"enabled": True,
|
||||
"models_used": [r.get("model") for r in results],
|
||||
"strategic_votes": strategic_votes,
|
||||
"total_votes": total_votes,
|
||||
"unanimous": strategic_votes == total_votes,
|
||||
"individual_results": [
|
||||
{
|
||||
"model": r.get("model"),
|
||||
"is_strategic": r.get("is_strategic"),
|
||||
"confidence": r.get("confidence", 0.0),
|
||||
"error": r.get("error")
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
print(f"[VOTING] Consensus: {strategic_votes}/{total_votes} voted strategic, final decision: {is_strategic_consensus}")
|
||||
|
||||
return final_result
|
||||
|
||||
def evaluate_strategic_impact(self, signal_content: str, relevant_context: List[dict] = None) -> dict:
|
||||
"""
|
||||
Evaluates a signal for strategic importance and generates an upgrade plan.
|
||||
|
||||
Args:
|
||||
signal_content: The external signal to evaluate
|
||||
relevant_context: Optional list of relevant ContextNodes (from RAG retrieval)
|
||||
Each dict should have: id, content, similarity, version, ai_insights
|
||||
|
||||
Returns:
|
||||
A dict with is_strategic, confidence, decision_proposal, competitiveness_radar, and tasks.
|
||||
"""
|
||||
# MOCK FALLBACK for Pixelle-Video
|
||||
if "Pixelle-Video" in signal_content:
|
||||
return {
|
||||
"is_strategic": True,
|
||||
"confidence": 0.95,
|
||||
"decision_proposal": {
|
||||
"situation_analysis": "Pixelle-Video is a core strategic asset for AI video generation. It aligns with our goal to dominate the automated content creation market.",
|
||||
"recommendation": "Immediate integration and optimization of Pixelle-Video engine.",
|
||||
"expected_benefits": "End-to-end video automation, reduced production costs by 90%, and competitive advantage in short video market."
|
||||
},
|
||||
"competitiveness_radar": {
|
||||
"current": [4, 3, 5, 4, 3],
|
||||
"projected": [9, 8, 8, 9, 8]
|
||||
},
|
||||
"detected_tech_stacks": [
|
||||
{"name": "FLUX", "category": "model", "relevance": "high"},
|
||||
{"name": "ComfyUI", "category": "framework", "relevance": "high"},
|
||||
{"name": "WAN 2.1", "category": "model", "relevance": "medium"},
|
||||
{"name": "Edge TTS", "category": "api", "relevance": "medium"},
|
||||
{"name": "FastAPI", "category": "framework", "relevance": "low"}
|
||||
],
|
||||
"related_knowledge_nodes": [
|
||||
{"title": "Pixelle-Video Pipeline Architecture", "category": "tech_stack", "summary": "Core video generation workflow with script-to-video automation"},
|
||||
{"title": "Competitor Video Generation Tools", "category": "competitor", "summary": "Analysis of Sora, Runway, and Pika Labs capabilities"},
|
||||
{"title": "Short Video Market Trends", "category": "market", "summary": "Growing demand for AI-automated content creation"}
|
||||
],
|
||||
"tasks": [
|
||||
{"task_name": "Integrate Pixelle-Video API into Main Platform", "type": "backend", "estimated_hours": 8.0},
|
||||
{"task_name": "Design Video Generation UI in Frontend", "type": "frontend", "estimated_hours": 12.0},
|
||||
{"task_name": "Optimize Video Rendering Pipeline", "type": "backend", "estimated_hours": 16.0},
|
||||
{"task_name": "Implement User Asset Management for Video", "type": "database", "estimated_hours": 4.0}
|
||||
]
|
||||
}
|
||||
|
||||
# Use the new helper method to build the prompt
|
||||
prompt = self._build_strategic_prompt(signal_content, relevant_context)
|
||||
|
||||
# Use the new helper method to evaluate with llama3
|
||||
result = self._evaluate_with_model("llama3", prompt)
|
||||
|
||||
# Remove the model field from the result for backward compatibility
|
||||
result.pop("model", None)
|
||||
|
||||
# Ensure required fields are present
|
||||
result.setdefault("detected_tech_stacks", [])
|
||||
result.setdefault("related_knowledge_nodes", [])
|
||||
|
||||
return result
|
||||
|
||||
def audit_code_change(self, task_name: str, commit_msg: str, code_diff: str = "") -> dict:
|
||||
"""
|
||||
Audits a code change against a task requirement.
|
||||
"""
|
||||
# MOCK FALLBACK for Pixelle-Video
|
||||
if "#PV-INIT" in commit_msg:
|
||||
return {
|
||||
"passed": True,
|
||||
"reason": "Initial codebase import verified. Pixelle-Video core structure detected.",
|
||||
"confidence": 0.99
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
You are a Senior Tech Lead performing an automated Code Audit.
|
||||
|
||||
Target Task: "{task_name}"
|
||||
Commit Message: "{commit_msg}"
|
||||
Code Changes (Snippet/Diff):
|
||||
```
|
||||
{code_diff[:2000]}
|
||||
```
|
||||
|
||||
Verify if this code change meaningfully addresses the task.
|
||||
Ignore minor formatting issues. Focus on functional implementation.
|
||||
|
||||
Required Output Format (JSON Only):
|
||||
{{
|
||||
"passed": boolean,
|
||||
"reason": "Short explanation of why it passed or failed",
|
||||
"confidence": float
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.client.post("/api/generate", json={
|
||||
"model": "llama3",
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
return {"passed": True, "reason": "Audit service unavailable, auto-approving for MVP", "confidence": 0.0}
|
||||
|
||||
data = response.json()
|
||||
response_text = data.get('response', '').strip()
|
||||
|
||||
import json
|
||||
result = json.loads(response_text)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Code Audit Failed: {e}")
|
||||
# Fallback for MVP: If commit message strongly claims fix, pass it?
|
||||
# For now, safe fail:
|
||||
return {"passed": False, "reason": f"Audit Error: {str(e)}", "confidence": 0.0}
|
||||
|
||||
def extract_context_insights(self, content: str) -> dict:
|
||||
"""
|
||||
Extracts strategic insights (Moat, Tech Specs) from project documentation.
|
||||
"""
|
||||
# MOCK FALLBACK for Pixelle-Video
|
||||
if "Pixelle-Video" in content:
|
||||
return {
|
||||
"moat": "Zero-Edit Automation: Fully automated pipeline from topic to video (Script -> Image -> TTS -> Video -> BGM).",
|
||||
"tech_specs": {
|
||||
"VRAM Usage": "~12GB (Optimized)",
|
||||
"Inference Speed": "30s per 10s video",
|
||||
"Models": "DeepSeek-V3, Wan2.1, Edge-TTS"
|
||||
}
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
Analyze the following technical documentation/context and extract key insights.
|
||||
|
||||
Content: "{content[:2000]}"...
|
||||
|
||||
Tasks:
|
||||
1. Identify the "Core Moat" (Unique Value Proposition/Technical Advantage).
|
||||
2. Extract key "Technical Specifications" (e.g., Performance, Requirements, Limits).
|
||||
|
||||
Required Output Format (JSON Only):
|
||||
{{
|
||||
"moat": "One sentence describing the core competitive advantage.",
|
||||
"tech_specs": {{
|
||||
"Param Name": "Value",
|
||||
"Param Name 2": "Value"
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.client.post("/api/generate", json={
|
||||
"model": "llama3",
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
return {}
|
||||
|
||||
data = response.json()
|
||||
response_text = data.get('response', '').strip()
|
||||
import json
|
||||
return json.loads(response_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Insight Extraction Failed: {e}")
|
||||
return {}
|
||||
|
||||
# Singleton instance
|
||||
llm_service = LLMService()
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import httpx
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class PlaneService:
|
||||
def __init__(self):
|
||||
self.api_url = os.getenv("PLANE_API_URL", "https://api.plane.so/api/v1")
|
||||
self.api_key = os.getenv("PLANE_API_KEY", "")
|
||||
self.workspace_slug = os.getenv("PLANE_WORKSPACE_SLUG", "simulation-workspace")
|
||||
self.project_id = os.getenv("PLANE_PROJECT_ID", "simulation-project-id")
|
||||
|
||||
# Enable simulation mode if no API key is provided
|
||||
self.is_simulation = not self.api_key or self.api_key == "mock"
|
||||
|
||||
async def get_label_id(self, label_name: str) -> str:
|
||||
"""
|
||||
Helper to get Label ID by name, or create if not exists.
|
||||
"""
|
||||
if self.is_simulation:
|
||||
return "simulated-label-id"
|
||||
|
||||
headers = {
|
||||
"X-API-Key": self.api_key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. List labels (Plane API usually returns all project labels)
|
||||
# Adjust URL based on Plane version: /workspaces/{slug}/projects/{id}/labels/ or /issue-labels/
|
||||
url = f"{self.api_url}/workspaces/{self.workspace_slug}/projects/{self.project_id}/issue-labels/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
labels = response.json()
|
||||
for label in labels:
|
||||
if label.get("name") == label_name:
|
||||
return label.get("id")
|
||||
|
||||
# 2. Create if not found
|
||||
# Random color generator or fixed
|
||||
payload = {"name": label_name, "color": "#ff0000"}
|
||||
create_res = await client.post(url, headers=headers, json=payload)
|
||||
if create_res.status_code == 201:
|
||||
return create_res.json().get("id")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to get/create label {label_name}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
async def create_issue(self, title: str, description: str, priority: str = "Medium", estimate: float = None, labels: List[str] = []) -> Dict[str, Any]:
|
||||
"""
|
||||
Creates an issue in Plane.
|
||||
"""
|
||||
if self.is_simulation:
|
||||
print(f"[SIMULATION] Creating Plane Issue: {title} ({estimate}h) - {priority} - Labels: {labels}")
|
||||
return {"id": "simulated-issue-id", "name": title, "status": "created (simulation)"}
|
||||
|
||||
headers = {
|
||||
"X-API-Key": self.api_key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# Map priority to Plane's expected format
|
||||
plane_priority = priority.lower()
|
||||
if plane_priority not in ["urgent", "high", "medium", "low", "none"]:
|
||||
plane_priority = "medium"
|
||||
|
||||
# Resolve Label IDs
|
||||
label_ids = []
|
||||
for label_name in labels:
|
||||
lid = await self.get_label_id(label_name)
|
||||
if lid:
|
||||
label_ids.append(lid)
|
||||
|
||||
payload = {
|
||||
"name": title,
|
||||
"description_html": f"<p>{description}</p>",
|
||||
"priority": plane_priority,
|
||||
"label_ids": label_ids
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
url = f"{self.api_url}/workspaces/{self.workspace_slug}/projects/{self.project_id}/issues/"
|
||||
response = await client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Failed to create Plane issue: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def update_issue_status(self, issue_identifier: str, status: str = "Done") -> Dict[str, Any]:
|
||||
"""
|
||||
Updates the status of a Plane issue.
|
||||
issue_identifier: Can be issue ID or Sequence ID (e.g., "PROJ-123")
|
||||
"""
|
||||
if self.is_simulation:
|
||||
print(f"[SIMULATION] Updating Plane Issue {issue_identifier} to status: {status}")
|
||||
return {"id": issue_identifier, "status": status, "simulated": True}
|
||||
|
||||
headers = {
|
||||
"X-API-Key": self.api_key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# First, we need to find the issue ID if a sequence ID is provided
|
||||
# Or if we assume issue_identifier is the UUID
|
||||
issue_id = issue_identifier
|
||||
|
||||
# If it looks like a sequence ID (e.g., PROJ-123), we might need to search for it first
|
||||
# For simplicity, let's assume we might need to search or use it directly if Plane supports it.
|
||||
# Plane API usually works with UUIDs for updates.
|
||||
# Let's try to search if it contains a hyphen and isn't a UUID
|
||||
if "-" in issue_identifier and len(issue_identifier) < 30:
|
||||
# Rough heuristic for sequence ID vs UUID
|
||||
# Search for the issue to get its UUID
|
||||
pass # TODO: Implement search logic if needed.
|
||||
# For now, let's assume the user provides the UUID or we just try our best.
|
||||
|
||||
# Plane uses State IDs for status updates, not just string names like "Done".
|
||||
# In a real implementation, we would need to fetch the State ID for "Done" from the project states.
|
||||
# For this MVP, we will simulate or assume a state_id is passed, or try to map it.
|
||||
# But since we can't easily map "Done" to a UUID without fetching states,
|
||||
# let's assume for now we are just patching the issue and Plane handles it or we mock it.
|
||||
|
||||
# Real-world: Fetch states -> Find ID for "Done" -> Update Issue
|
||||
# Simplified: Just log the intent or try to update if we had the ID.
|
||||
|
||||
print(f"Warning: To update status to '{status}' in Plane, we need the specific State UUID.")
|
||||
print(f"Skipping actual API call for status update in this MVP version to avoid 400 errors without State IDs.")
|
||||
|
||||
return {"status": "skipped_implementation_gap", "message": "State ID lookup required for real update"}
|
||||
|
||||
async def sync_tasks(self, tasks: List[Dict[str, Any]], is_urgent: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Syncs a list of tasks to Plane.
|
||||
Returns details about created tasks including IDs.
|
||||
"""
|
||||
created_count = 0
|
||||
errors = []
|
||||
created_tasks_details = []
|
||||
|
||||
# Determine priority and labels based on urgency
|
||||
priority = "Urgent" if is_urgent else "Medium"
|
||||
labels = ["Signal-Driven"] if is_urgent else []
|
||||
|
||||
for task in tasks:
|
||||
description = f"Type: {task.get('type', 'General')}<br>Estimated Hours: {task.get('estimated_hours', 0)}"
|
||||
|
||||
result = await self.create_issue(
|
||||
title=task.get("task_name", "Untitled Task"),
|
||||
description=description,
|
||||
priority=priority,
|
||||
estimate=task.get("estimated_hours"),
|
||||
labels=labels
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
errors.append(f"Failed to create {task.get('task_name')}: {result['error']}")
|
||||
else:
|
||||
created_count += 1
|
||||
# Append task with its new Plane ID (simulated or real)
|
||||
# result contains the issue object from Plane API
|
||||
task_with_id = task.copy()
|
||||
task_with_id["plane_id"] = result.get("id")
|
||||
# Also try to get a sequence ID if available (e.g. "PROJ-123")
|
||||
task_with_id["plane_key"] = result.get("sequence_id") or result.get("identifier") or result.get("id")
|
||||
task_with_id["status"] = "Todo" # Initial status
|
||||
created_tasks_details.append(task_with_id)
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial_success",
|
||||
"created_count": created_count,
|
||||
"errors": errors,
|
||||
"created_tasks": created_tasks_details,
|
||||
"simulation_mode": self.is_simulation
|
||||
}
|
||||
|
||||
# Singleton
|
||||
plane_service = PlaneService()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
psycopg2-binary
|
||||
pydantic
|
||||
alembic
|
||||
python-dotenv
|
||||
asyncpg
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import AsyncSessionLocal, engine
|
||||
from app.models import Signal, ContextNode, Experiment
|
||||
from sqlalchemy import text
|
||||
import uuid
|
||||
|
||||
async def seed_data():
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 1. Clear existing data (optional, but good for a clean start)
|
||||
await session.execute(text("TRUNCATE TABLE signals, context_nodes, experiments RESTART IDENTITY CASCADE;"))
|
||||
|
||||
# 2. Seed Signals (外部信号 - 触发工作的源头)
|
||||
signals = [
|
||||
Signal(
|
||||
source="HackerNews",
|
||||
content="DeepSeek-V3 release: 671B parameters, beats GPT-4 on coding tasks.",
|
||||
impact_score=0.95,
|
||||
processed=False
|
||||
),
|
||||
Signal(
|
||||
source="Internal User Feedback",
|
||||
content="RAG retrieval is too slow (avg 3s), users are dropping off.",
|
||||
impact_score=0.85,
|
||||
processed=False
|
||||
),
|
||||
Signal(
|
||||
source="Arxiv",
|
||||
content="Paper: 'Lost in the Middle: How Language Models Use Long Contexts'",
|
||||
impact_score=0.6,
|
||||
processed=True
|
||||
)
|
||||
]
|
||||
session.add_all(signals)
|
||||
|
||||
# 3. Seed Context Nodes (团队当前的认知/共识)
|
||||
context_nodes = [
|
||||
ContextNode(
|
||||
content="当前 RAG 切片大小设定为 512 tokens,重叠 50 tokens。",
|
||||
version=1,
|
||||
status="active"
|
||||
),
|
||||
ContextNode(
|
||||
content="所有 SQL 生成任务默认使用 gpt-4-turbo,因为其指令遵循能力最强。",
|
||||
version=2,
|
||||
status="active"
|
||||
),
|
||||
ContextNode(
|
||||
content="向量数据库使用 pgvector,当前索引类型为 ivfflat。",
|
||||
version=1,
|
||||
status="active"
|
||||
)
|
||||
]
|
||||
session.add_all(context_nodes)
|
||||
|
||||
# 4. Seed Experiments (基于信号和认知制定的行动)
|
||||
experiments = [
|
||||
Experiment(
|
||||
title="评估 DeepSeek-V3 的 SQL 生成能力",
|
||||
hypothesis="如果 DeepSeek-V3 在 Spider 数据集上表现达到 95% GPT-4 水平,我们可以替换 GPT-4 以降低 90% 成本。",
|
||||
status="running",
|
||||
target_metric="accuracy_vs_cost",
|
||||
current_score=0.88
|
||||
),
|
||||
Experiment(
|
||||
title="RAG 延迟优化:混合检索策略",
|
||||
hypothesis="引入 BM25 稀疏检索 + 向量检索的混合模式,可以在保持召回率的同时减少重排序候选集,降低延迟。",
|
||||
status="draft",
|
||||
target_metric="latency_p95",
|
||||
current_score=None
|
||||
),
|
||||
Experiment(
|
||||
title="上下文窗口扩展测试",
|
||||
hypothesis="将上下文窗口扩展到 128k,测试在大文档问答中的准确率变化。",
|
||||
status="failed",
|
||||
target_metric="recall",
|
||||
current_score=0.45
|
||||
)
|
||||
]
|
||||
session.add_all(experiments)
|
||||
|
||||
await session.commit()
|
||||
print("Database seeded successfully with initial data!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_data())
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Neural Sync: AI 原生项目管理平台设计方案
|
||||
> **目标用户**: 10 人精英 AI 技术团队 | **核心价值**: 认知对齐 & 极速响应 | **原则**: 拒绝外部开源工具臃肿堆砌,全自研轻量化核心
|
||||
|
||||
## 1. 原始方案深度审计
|
||||
你原有的方案依赖于集成 **Plane (项目管理)** 和 **MaxKB (知识库)**。
|
||||
**为什么这违背了“AI Native”的目标:**
|
||||
* **数据孤岛 (Siloed Data)**:需求躺在 Plane 里,知识躺在 MaxKB 里,代码躺在 Git 里。AI 需要跨越这三座孤岛去“搬运”信息,这本身就制造了巨大的认知延迟和错误率,而不是消灭它。
|
||||
* **静态与动态的错位**: Plane 管理的是“任务 (Tasks)”(静态的),但 AI 开发管理的是“实验 (Experiments)”(动态的)。在传统软件中,代码合并即任务完成;但在 AI 开发中,只有**评测分数 (Eval Score)** 提升了,实验才算成功。Plane 无法原生追踪评测分数。
|
||||
* **维护黑洞**: 你只有 10 个人,却要维护两套复杂的开源系统。光是修这两个系统的 Bug 和适配 API 变动,就会消耗掉你 2 个人的战力。
|
||||
|
||||
## 2. 优化后的核心哲学:“活体上下文 (Living Context)”
|
||||
我们不再管理“任务”,而是管理 **“上下文 (Context) & 评测 (Evals)”**。
|
||||
* **上下文 (Context)**: 我们已知的信息(市场洞察、模型能力边界、技术约束)。
|
||||
* **评测 (Evals)**: 我们如何定义成功(准确率、延迟、用户留存)。
|
||||
* **假设 (Hypothesis)**: 所谓的“任务”,本质上只是一个基于当前上下文,试图提升评测指标的“假设”。
|
||||
|
||||
## 3. 系统架构 (Neural Sync 引擎)
|
||||
|
||||
### 3.1 技术栈 (统一且 Python 原生)
|
||||
* **前端**: Vue 3 + Naive UI (比 Element Plus 更轻量,更适合数据密集型应用)。
|
||||
* **后端**: **FastAPI** (Python)。 *理由:你的 10 人团队都是 AI 专家,Python 是母语。不要强迫他们为了内部工具去写 Java (Spring Boot)。这让你们可以直接在后端集成模型训练/评测流水线。*
|
||||
* **数据库**: PostgreSQL (带 `pgvector` 扩展)。统一存储关系型数据(用户、任务)和语义数据(RAG 向量)。
|
||||
* **编排**: 轻量级事件总线 (Event Bus) (例如内部 Python `asyncio` 队列 或 Redis Streams)。
|
||||
|
||||
### 3.2 核心模块 (全自研,无开源 PM 工具)
|
||||
|
||||
#### 模块 A: “脉搏” (The Pulse - 市场与模型信号接收)
|
||||
* **功能**: 替代“市场监控”。
|
||||
* **机制**:
|
||||
* **信号源**: 自动爬虫 (新闻、论文、竞品更新) + 内部模型评测日志。
|
||||
* **AI Agent ("侦察兵")**: 分析信号 -> 生成 `洞察 (Insight)`。
|
||||
* **动作**: 如果 `洞察` 与当前的 `计划` 相冲突,触发 **“对齐警报 (Alignment Alert)”**。
|
||||
|
||||
#### 模块 B: “认知核心” (The Cognitive Core - 活体说明书)
|
||||
* **功能**: 替代“Wiki/MaxKB”。
|
||||
* **机制**:
|
||||
* **原子化知识**: 信息以 `ContextNode (知识节点)` 形式存储 (例如:“我们的 RAG 切片大小是 512”)。
|
||||
* **版本化**: 每个节点都有版本历史。
|
||||
* **强关联**: 任务直接挂载在节点上。如果节点发生变更 (切片大小 -> 1024),所有关联的任务自动标记为“已过时 (Outdated)”。
|
||||
|
||||
#### 模块 C: “实验室” (The Lab - 评测驱动的任务管理)
|
||||
* **功能**: 替代“Plane/Jira”。
|
||||
* **机制**:
|
||||
* **实体**: `Experiment (实验)`。
|
||||
* **工作流**: `提出假设` -> `代码实现` -> `自动化评测` -> `决策`。
|
||||
* **仪表盘**: 不是看板 (Kanban),而是实验的 **“排行榜 (Leaderboard)”** 及其对核心指标的影响。
|
||||
|
||||
## 4. 数据库设计 (PostgreSQL) - 唯一的真理来源
|
||||
|
||||
```sql
|
||||
-- 1. 认知的原子单位
|
||||
CREATE TABLE context_nodes (
|
||||
id UUID PRIMARY KEY,
|
||||
content TEXT NOT NULL, -- 实际知识内容 (例如: "用户画像: 数据科学家")
|
||||
embedding VECTOR(1536), -- 用于语义搜索/RAG
|
||||
version INT DEFAULT 1,
|
||||
status VARCHAR(20) DEFAULT 'active', -- active, deprecated
|
||||
created_by UUID REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 2. "实验" (即传统意义上的任务)
|
||||
CREATE TABLE experiments (
|
||||
id UUID PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
hypothesis TEXT, -- "假设:如果我们增加切片大小,召回率会提升。"
|
||||
status VARCHAR(50), -- 'draft', 'running', 'evaluating', 'success', 'failed'
|
||||
owner_id UUID REFERENCES users(id),
|
||||
linked_context_nodes UUID[], -- 依赖的知识节点
|
||||
target_metric VARCHAR(50), -- 例如: 'rag_accuracy'
|
||||
current_score FLOAT -- 来自 CI/CD 的实时分数
|
||||
);
|
||||
|
||||
-- 3. 信号 (外部触发器)
|
||||
CREATE TABLE signals (
|
||||
id UUID PRIMARY KEY,
|
||||
source VARCHAR(50), -- 'market_news', 'model_update', 'competitor_release'
|
||||
content TEXT,
|
||||
impact_score FLOAT, -- AI 评估的紧急程度 (0-1)
|
||||
processed BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
```
|
||||
|
||||
## 5. 工作流:“零负担”闭环
|
||||
|
||||
1. **信号**: “OpenAI 发布了 GPT-5”。
|
||||
2. **Agent (侦察兵)**: 创建一条 `Signal` 记录。标记为高影响分。
|
||||
3. **Agent (对齐者)**: 检查 `ContextNodes`。发现节点:“基座模型: GPT-4”。标记该节点为“潜在过时”。
|
||||
4. **通知**: 技术负责人收到 Ping:“信号 GPT-5 与知识节点 '基座模型' 冲突。是否创建实验?”
|
||||
5. **动作**: 负责人点击“是”。系统自动创建 `Experiment`:“评估 GPT-5 在代码生成上的表现”。
|
||||
6. **执行**: 开发者实现代码。提交 Commit。
|
||||
7. **评测**: CI 流水线运行 Benchmark。更新 `experiments.current_score`。
|
||||
8. **闭环**: 分数 > 基线。实验标记为“成功”。`ContextNode` 自动更新为“基座模型: GPT-5”。
|
||||
|
||||
## 6. 为什么这最适合你们
|
||||
* **10 人团队**: 不需要复杂的权限/角色管理。速度是唯一指标。
|
||||
* **认知同步**: `ContextNode` 的强关联机制确保了当“基座模型”变更时,*每个人*都知道,因为他们的任务会变红(被标记)。
|
||||
* **无开源臃肿**: 你只需要建立这 3 张表和少量的 Python 接口。快速、干净、完全可控。
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.5",
|
||||
"echarts": "^6.0.0",
|
||||
"marked": "^17.0.3",
|
||||
"naive-ui": "^2.43.2",
|
||||
"vfonts": "^0.0.3",
|
||||
"vue": "^3.5.25",
|
||||
"vue-echarts": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0-beta.13",
|
||||
"vue-tsc": "^3.1.5"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.13"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<n-config-provider :theme="theme">
|
||||
<n-message-provider>
|
||||
<n-layout style="height: 100vh">
|
||||
<n-layout-header bordered style="padding: 16px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<n-text tag="div" class="logo">
|
||||
<n-gradient-text type="info" :size="24">Neural Sync</n-gradient-text>
|
||||
</n-text>
|
||||
<n-menu mode="horizontal" :options="menuOptions" v-model:value="activeKey" responsive />
|
||||
</n-layout-header>
|
||||
<n-layout-content content-style="padding: 16px;">
|
||||
<component :is="currentComponent" />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { NConfigProvider, NMessageProvider, NLayout, NLayoutHeader, NLayoutContent, NGradientText, NMenu, NText } from 'naive-ui'
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import SignalDashboard from './components/SignalDashboard.vue'
|
||||
import ExperimentBoard from './components/ExperimentBoard.vue'
|
||||
import ContextManager from './components/ContextManager.vue'
|
||||
|
||||
const theme = ref(darkTheme)
|
||||
const activeKey = ref('signals')
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{ label: '信号总线', key: 'signals' },
|
||||
{ label: '实验看板', key: 'experiments' },
|
||||
{ label: '认知核心', key: 'context' }
|
||||
]
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
switch (activeKey.value) {
|
||||
case 'signals': return SignalDashboard
|
||||
case 'experiments': return ExperimentBoard
|
||||
case 'context': return ContextManager
|
||||
default: return SignalDashboard
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
|
|
@ -0,0 +1,321 @@
|
|||
<template>
|
||||
<n-space vertical>
|
||||
<n-card title="活体上下文 (认知节点)" size="medium" class="glass-card">
|
||||
<template #header-extra>
|
||||
<n-button type="primary" @click="fetchNodes" ghost>刷新</n-button>
|
||||
<n-button @click="showModal = true" style="margin-left: 12px;" ghost>新增认知</n-button>
|
||||
</template>
|
||||
|
||||
<n-collapse accordion>
|
||||
<n-collapse-item v-for="node in parsedNodes" :key="node.id" :name="node.id">
|
||||
<template #header>
|
||||
<div class="node-header">
|
||||
<n-tag :type="node.status === 'active' ? 'success' : 'warning'" size="small" round style="margin-right: 12px;">
|
||||
v{{ node.version }}
|
||||
</n-tag>
|
||||
<span class="node-title">{{ node.extractedTitle || '未命名节点' }}</span>
|
||||
<n-tag v-if="node.techStack.length" size="tiny" type="info" ghost style="margin-left: 12px;">
|
||||
{{ node.techStack.length }} 技术栈
|
||||
</n-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="node-content">
|
||||
<!-- AI Insights Section -->
|
||||
<div v-if="node.ai_insights && (node.ai_insights.moat || node.ai_insights.tech_specs)" class="section-block insight-block">
|
||||
<div class="section-title">✨ AI 核心洞察 (Insights)</div>
|
||||
|
||||
<!-- Core Moat -->
|
||||
<div v-if="node.ai_insights.moat" class="insight-item">
|
||||
<div class="insight-label">🛡️ 核心护城河</div>
|
||||
<div class="insight-value">{{ node.ai_insights.moat }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Tech Specs -->
|
||||
<div v-if="node.ai_insights.tech_specs" class="insight-specs">
|
||||
<div class="insight-label">⚙️ 关键技术参数</div>
|
||||
<n-grid :x-gap="12" :y-gap="8" :cols="2">
|
||||
<n-grid-item v-for="(val, key) in node.ai_insights.tech_specs" :key="key">
|
||||
<div class="spec-card">
|
||||
<span class="spec-key">{{ key }}</span>
|
||||
<span class="spec-val">{{ val }}</span>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tech Stack Section -->
|
||||
<div v-if="node.techStack.length" class="section-block">
|
||||
<div class="section-title">🛠️ 技术栈 (Tech Stack)</div>
|
||||
<n-space size="small">
|
||||
<n-tag v-for="tech in node.techStack" :key="tech" size="small" type="info" secondary>
|
||||
{{ tech }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- Core Logic Section -->
|
||||
<div v-if="node.coreLogic" class="section-block">
|
||||
<div class="section-title">🧠 核心逻辑 (Core Logic)</div>
|
||||
<div class="core-logic-text">{{ node.coreLogic }}</div>
|
||||
</div>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<!-- Full Markdown Content -->
|
||||
<div class="markdown-body" v-html="node.htmlContent"></div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card style="width: 600px" title="定义新认知" :bordered="false" size="huge" role="dialog" aria-modal="true" class="glass-modal">
|
||||
<n-form ref="formRef" :model="formValue">
|
||||
<n-form-item label="内容 (Markdown)" path="content">
|
||||
<n-input v-model:value="formValue.content" type="textarea" placeholder="# 标题\n\n## 技术栈\n- Python\n- Vue\n\n## 核心逻辑\n..." :rows="10" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-button type="primary" block @click="createNode">提交认知</n-button>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { NSpace, NCard, NButton, NCollapse, NCollapseItem, NTag, NModal, NForm, NFormItem, NInput, useMessage, NDivider, NGrid, NGridItem } from 'naive-ui'
|
||||
import axios from 'axios'
|
||||
import { marked } from 'marked'
|
||||
|
||||
const message = useMessage()
|
||||
const nodes = ref<any[]>([])
|
||||
const showModal = ref(false)
|
||||
const formValue = ref({
|
||||
content: ''
|
||||
})
|
||||
|
||||
// Heuristic Extraction
|
||||
const extractMetadata = (content: string) => {
|
||||
const lines = content.split('\n')
|
||||
let title = ''
|
||||
let techStack: string[] = []
|
||||
let coreLogic = ''
|
||||
|
||||
// 1. Extract Title (First H1 or first non-empty line)
|
||||
const titleMatch = content.match(/^#\s+(.*)/m)
|
||||
if (titleMatch) {
|
||||
title = titleMatch[1].trim()
|
||||
} else {
|
||||
const firstLine = lines.find(l => l.trim().length > 0)
|
||||
if (firstLine) title = firstLine.replace(/^#+\s+/, '').substring(0, 50)
|
||||
}
|
||||
|
||||
// 1.5 Extract AI Insights (If stored in JSON in backend but not parsed here, we rely on backend data)
|
||||
// This frontend extractor is for 'raw markdown' metadata.
|
||||
// The 'node' object from API already has 'ai_insights'.
|
||||
// We just need to ensure we don't overwrite it or we display it.
|
||||
|
||||
// 2. Extract Tech Stack (Keywords or Section)
|
||||
// Simple keyword search for now
|
||||
const keywords = ['Python', 'Vue', 'FastAPI', 'PostgreSQL', 'SQLite', 'Redis', 'Docker', 'Kubernetes', 'Ollama', 'LLM', 'RAG', 'LangChain', 'ComfyUI']
|
||||
const foundKeywords = new Set<string>()
|
||||
keywords.forEach(k => {
|
||||
if (content.toLowerCase().includes(k.toLowerCase())) {
|
||||
foundKeywords.add(k)
|
||||
}
|
||||
})
|
||||
// Also look for explicit list items under "Tech" header
|
||||
// (Simplified: just use keywords for robustness)
|
||||
techStack = Array.from(foundKeywords)
|
||||
|
||||
// 3. Extract Core Logic (Summary)
|
||||
// Look for "Core Logic", "Architecture", "Introduction" headers
|
||||
const logicRegex = /##\s*(Core Logic|Architecture|Introduction|About|Features)([\s\S]*?)(##|$)/i
|
||||
const logicMatch = content.match(logicRegex)
|
||||
if (logicMatch) {
|
||||
coreLogic = logicMatch[2].trim().substring(0, 300) + (logicMatch[2].length > 300 ? '...' : '')
|
||||
} else {
|
||||
// Fallback: First paragraph after title
|
||||
const paragraphs = content.split(/\n\s*\n/)
|
||||
if (paragraphs.length > 1) {
|
||||
coreLogic = paragraphs[1].trim().substring(0, 200) + '...'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
techStack,
|
||||
coreLogic,
|
||||
htmlContent: marked.parse(content)
|
||||
}
|
||||
}
|
||||
|
||||
const parsedNodes = computed(() => {
|
||||
return nodes.value.map(node => {
|
||||
const meta = extractMetadata(node.content)
|
||||
return {
|
||||
...node,
|
||||
extractedTitle: meta.title,
|
||||
techStack: meta.techStack,
|
||||
coreLogic: meta.coreLogic,
|
||||
htmlContent: meta.htmlContent
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const fetchNodes = async () => {
|
||||
try {
|
||||
const res = await axios.get('http://127.0.0.1:8000/context_nodes/')
|
||||
nodes.value = res.data
|
||||
} catch (error) {
|
||||
message.error('获取认知节点失败')
|
||||
}
|
||||
}
|
||||
|
||||
const createNode = async () => {
|
||||
try {
|
||||
const res = await axios.post('http://127.0.0.1:8000/context_nodes/', formValue.value)
|
||||
message.success('认知已提交!')
|
||||
nodes.value.push(res.data)
|
||||
showModal.value = false
|
||||
formValue.value.content = ''
|
||||
} catch (error) {
|
||||
message.error('创建认知节点失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchNodes)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass-card {
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Insights Styling */
|
||||
.insight-block {
|
||||
background: rgba(99, 226, 183, 0.1);
|
||||
border: 1px solid rgba(99, 226, 183, 0.2);
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.insight-label {
|
||||
font-size: 11px;
|
||||
color: #63e2b7;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.insight-value {
|
||||
font-size: 14px;
|
||||
color: #e2e8f0;
|
||||
font-style: italic;
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid #63e2b7;
|
||||
}
|
||||
|
||||
.insight-specs {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.spec-card {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spec-key {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.spec-val {
|
||||
font-size: 12px;
|
||||
color: #63e2b7;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-bottom: 16px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.core-logic-text {
|
||||
font-size: 14px;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
color: #cbd5e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3) {
|
||||
color: #e2e8f0;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(p) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(ul),
|
||||
.markdown-body :deep(ol) {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(code) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre) {
|
||||
background: #0f172a;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<n-space vertical>
|
||||
<n-card title="进行中的实验" size="medium">
|
||||
<template #header-extra>
|
||||
<n-button type="primary" @click="fetchExperiments">刷新</n-button>
|
||||
<n-button @click="openModal" style="margin-left: 12px;">新建实验</n-button>
|
||||
</template>
|
||||
|
||||
<n-grid :x-gap="12" :y-gap="12" :cols="3">
|
||||
<n-grid-item v-for="exp in experiments" :key="exp.id">
|
||||
<n-card :title="exp.title" hoverable>
|
||||
<template #header-extra>
|
||||
<n-tag :type="getStatusType(exp.status)">{{ getStatusLabel(exp.status) }}</n-tag>
|
||||
</template>
|
||||
<p class="hypothesis-text">{{ exp.hypothesis }}</p>
|
||||
<n-space vertical>
|
||||
<n-statistic label="当前评分" :value="exp.current_score || '暂无数据'" />
|
||||
<n-text depth="3" v-if="exp.linked_context_nodes && exp.linked_context_nodes.length">
|
||||
关联认知节点: {{ exp.linked_context_nodes.length }} 个
|
||||
</n-text>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card style="width: 600px" title="设计新实验" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
||||
<n-form ref="formRef" :model="formValue">
|
||||
<n-form-item label="标题" path="title">
|
||||
<n-input v-model:value="formValue.title" placeholder="例如: 测试 DeepSeek-V3 的 SQL 生成能力" />
|
||||
</n-form-item>
|
||||
<n-form-item label="假设 (Hypothesis)" path="hypothesis">
|
||||
<n-input v-model:value="formValue.hypothesis" type="textarea" placeholder="如果我们切换到 DeepSeek-V3,准确率预计提升 5%..." />
|
||||
</n-form-item>
|
||||
<n-form-item label="目标指标" path="target_metric">
|
||||
<n-input v-model:value="formValue.target_metric" placeholder="例如: accuracy, latency" />
|
||||
</n-form-item>
|
||||
<n-form-item label="关联认知节点 (Context)" path="linked_context_nodes">
|
||||
<n-select
|
||||
v-model:value="formValue.linked_context_nodes"
|
||||
multiple
|
||||
:options="contextNodeOptions"
|
||||
placeholder="选择此实验试图挑战或验证的认知..."
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-button type="primary" block @click="createExperiment">启动实验</n-button>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { NSpace, NCard, NButton, NGrid, NGridItem, NTag, NStatistic, NModal, NForm, NFormItem, NInput, useMessage, NSelect, NText } from 'naive-ui'
|
||||
import axios from 'axios'
|
||||
|
||||
const message = useMessage()
|
||||
const experiments = ref<any[]>([])
|
||||
const contextNodes = ref<any[]>([])
|
||||
const showModal = ref(false)
|
||||
const formValue = ref({
|
||||
title: '',
|
||||
hypothesis: '',
|
||||
target_metric: 'accuracy',
|
||||
linked_context_nodes: []
|
||||
})
|
||||
|
||||
const contextNodeOptions = computed(() => {
|
||||
return contextNodes.value.map(node => ({
|
||||
label: `${node.content.substring(0, 50)}...`,
|
||||
value: node.id
|
||||
}))
|
||||
})
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success'
|
||||
case 'failed': return 'error'
|
||||
case 'running': return 'info'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return '成功'
|
||||
case 'failed': return '失败'
|
||||
case 'running': return '进行中'
|
||||
case 'draft': return '草稿'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExperiments = async () => {
|
||||
try {
|
||||
const res = await axios.get('http://127.0.0.1:8000/experiments/')
|
||||
experiments.value = res.data
|
||||
} catch (error) {
|
||||
message.error('获取实验列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const fetchContextNodes = async () => {
|
||||
try {
|
||||
const res = await axios.get('http://127.0.0.1:8000/context_nodes/')
|
||||
contextNodes.value = res.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch context nodes')
|
||||
}
|
||||
}
|
||||
|
||||
const openModal = () => {
|
||||
fetchContextNodes()
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const createExperiment = async () => {
|
||||
try {
|
||||
const res = await axios.post('http://127.0.0.1:8000/experiments/', formValue.value)
|
||||
message.success('实验已启动!')
|
||||
experiments.value.push(res.data)
|
||||
showModal.value = false
|
||||
// Reset form
|
||||
formValue.value = {
|
||||
title: '',
|
||||
hypothesis: '',
|
||||
target_metric: 'accuracy',
|
||||
linked_context_nodes: []
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('创建实验失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchExperiments()
|
||||
fetchContextNodes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hypothesis-text {
|
||||
min-height: 3em;
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,5 @@
|
|||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Neural Sync 启动器 - 跨平台版本
|
||||
一键启动前后端服务
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import signal
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# 颜色输出
|
||||
class Colors:
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
RED = '\033[0;31m'
|
||||
BLUE = '\033[0;34m'
|
||||
NC = '\033[0m'
|
||||
|
||||
def print_colored(color, text):
|
||||
print(f"{color}{text}{Colors.NC}")
|
||||
|
||||
def get_project_dir():
|
||||
return Path(__file__).parent
|
||||
|
||||
def check_venv():
|
||||
venv_dir = get_project_dir() / "venv"
|
||||
if not venv_dir.exists():
|
||||
print_colored(Colors.RED, "❌ 虚拟环境不存在,请先创建: python -m venv venv")
|
||||
sys.exit(1)
|
||||
|
||||
def kill_port(port):
|
||||
"""终止占用指定端口的进程"""
|
||||
system = platform.system()
|
||||
try:
|
||||
if system == "Darwin": # macOS
|
||||
subprocess.run(["lsof", "-ti", f"{port}"],
|
||||
capture_output=True, text=True, check=True)
|
||||
result = subprocess.run(["lsof", "-ti", f"{port}"],
|
||||
capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
subprocess.run(["kill", "-9"] + result.stdout.strip().split(),
|
||||
capture_output=True)
|
||||
elif system == "Linux":
|
||||
result = subprocess.run(["fuser", "-k", f"{port}/tcp"],
|
||||
capture_output=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
def start_backend():
|
||||
"""启动后端服务"""
|
||||
print_colored(Colors.GREEN, "▶️ 启动后端服务...")
|
||||
|
||||
project_dir = get_project_dir()
|
||||
backend_dir = project_dir / "backend"
|
||||
log_dir = project_dir / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 激活虚拟环境并启动
|
||||
venv_python = get_project_dir() / "venv" / "bin" / "python"
|
||||
if platform.system() == "Windows":
|
||||
venv_python = get_project_dir() / "venv" / "Scripts" / "python.exe"
|
||||
|
||||
log_file = log_dir / "backend.log"
|
||||
|
||||
process = subprocess.Popen(
|
||||
[str(venv_python), "-m", "uvicorn", "app.main:app", "--reload", "--port", "8000"],
|
||||
cwd=backend_dir,
|
||||
stdout=open(log_file, "w"),
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
# 等待后端启动
|
||||
for i in range(10):
|
||||
try:
|
||||
response = subprocess.run(
|
||||
["curl", "-s", "http://127.0.0.1:8000/docs"],
|
||||
capture_output=True, timeout=2
|
||||
)
|
||||
if response.returncode == 0:
|
||||
print_colored(Colors.GREEN, f"✅ 后端启动成功 (PID: {process.pid})")
|
||||
return process
|
||||
except:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
print_colored(Colors.RED, "❌ 后端启动失败")
|
||||
return None
|
||||
|
||||
def start_frontend():
|
||||
"""启动前端服务"""
|
||||
print_colored(Colors.GREEN, "▶️ 启动前端服务...")
|
||||
|
||||
project_dir = get_project_dir()
|
||||
frontend_dir = project_dir / "frontend"
|
||||
log_dir = project_dir / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
log_file = log_dir / "frontend.log"
|
||||
|
||||
process = subprocess.Popen(
|
||||
["npm", "run", "dev", "--", "--host"],
|
||||
cwd=frontend_dir,
|
||||
stdout=open(log_file, "w"),
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=True
|
||||
)
|
||||
|
||||
# 等待前端启动
|
||||
for i in range(15):
|
||||
try:
|
||||
response = subprocess.run(
|
||||
["curl", "-s", "http://127.0.0.1:5173"],
|
||||
capture_output=True, timeout=2
|
||||
)
|
||||
if response.returncode == 0:
|
||||
print_colored(Colors.GREEN, f"✅ 前端启动成功 (PID: {process.pid})")
|
||||
return process
|
||||
except:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
print_colored(Colors.YELLOW, "⚠️ 前端可能还在启动中")
|
||||
return process
|
||||
|
||||
def main():
|
||||
print_colored(Colors.BLUE, "🚀 Neural Sync 启动器")
|
||||
print("=" * 30)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
os.chdir(project_dir)
|
||||
|
||||
# 检查环境
|
||||
check_venv()
|
||||
|
||||
# 清理端口
|
||||
print_colored(Colors.BLUE, "🧹 清理端口...")
|
||||
kill_port(8000)
|
||||
kill_port(5173)
|
||||
time.sleep(1)
|
||||
|
||||
# 启动服务
|
||||
backend_proc = start_backend()
|
||||
if not backend_proc:
|
||||
sys.exit(1)
|
||||
|
||||
frontend_proc = start_frontend()
|
||||
|
||||
# 保存 PID
|
||||
log_dir = project_dir / "logs"
|
||||
(log_dir / "backend.pid").write_text(str(backend_proc.pid))
|
||||
(log_dir / "frontend.pid").write_text(str(frontend_proc.pid))
|
||||
|
||||
# 显示结果
|
||||
print()
|
||||
print_colored(Colors.GREEN, "🎉 所有服务启动完成!")
|
||||
print("=" * 30)
|
||||
print(f" 后端: {Colors.YELLOW}http://127.0.0.1:8000{Colors.NC}")
|
||||
print(f" 前端: {Colors.YELLOW}http://127.0.0.1:5173{Colors.NC}")
|
||||
print(f" API 文档: {Colors.YELLOW}http://127.0.0.1:8000/docs{Colors.NC}")
|
||||
print()
|
||||
print("📝 日志文件:")
|
||||
print(" 后端: logs/backend.log")
|
||||
print(" 前端: logs/frontend.log")
|
||||
print()
|
||||
print_colored(Colors.YELLOW, "💡 按 Ctrl+C 停止所有服务,或运行: python stop.py")
|
||||
|
||||
# 等待中断信号
|
||||
def signal_handler(sig, frame):
|
||||
print_colored(Colors.YELLOW, "\n🛑 正在停止服务...")
|
||||
backend_proc.terminate()
|
||||
frontend_proc.terminate()
|
||||
print_colored(Colors.GREEN, "🎉 所有服务已停止")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# 保持运行
|
||||
try:
|
||||
backend_proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(None, None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
|
||||
import asyncio
|
||||
import httpx
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
API_BASE = "http://127.0.0.1:8000"
|
||||
PIXELLE_PATH = os.path.abspath("Pixelle-Video")
|
||||
|
||||
async def main():
|
||||
print(f"Integrating Pixelle-Video from {PIXELLE_PATH}...")
|
||||
|
||||
# 1. Read README for Context
|
||||
readme_path = os.path.join(PIXELLE_PATH, "README.md")
|
||||
if not os.path.exists(readme_path):
|
||||
print("README.md not found, using default description.")
|
||||
description = "Pixelle-Video: AI Automated Short Video Engine."
|
||||
else:
|
||||
with open(readme_path, "r") as f:
|
||||
description = f.read()[:2000] # Limit length
|
||||
|
||||
# 2. Create Context Node
|
||||
async with httpx.AsyncClient(trust_env=False) as client:
|
||||
print("Creating Context Node...")
|
||||
try:
|
||||
resp = await client.post(f"{API_BASE}/context_nodes/", json={
|
||||
"content": f"Project Context: Pixelle-Video\n\n{description}"
|
||||
})
|
||||
if resp.status_code == 200:
|
||||
print(f"Context Node Created: {resp.json().get('id')}")
|
||||
else:
|
||||
print(f"Failed to create Context Node: {resp.status_code} {resp.text}")
|
||||
except Exception as e:
|
||||
print(f"Error creating Context Node: {e}")
|
||||
|
||||
# 3. Ingest Signal
|
||||
print("Ingesting Project Signal...")
|
||||
try:
|
||||
resp = await client.post(f"{API_BASE}/signals/ingest", json={
|
||||
"source": "System Integration",
|
||||
"content": f"New Project Detected: Pixelle-Video. Path: {PIXELLE_PATH}. Status: Active Development. Requires strategic assessment and task breakdown.",
|
||||
"impact_score": 0.95
|
||||
})
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
print(f"Signal Ingested: {data.get('id')}")
|
||||
print(f"Strategic Analysis: {data.get('decision_proposal')}")
|
||||
print(f"Tasks Generated: {data.get('tasks_generated')}")
|
||||
else:
|
||||
print(f"Failed to ingest signal: {resp.text}")
|
||||
except Exception as e:
|
||||
print(f"Error ingesting signal: {e}")
|
||||
|
||||
# 4. Simulate Git Webhook (Code Audit)
|
||||
print("Simulating Code Audit...")
|
||||
try:
|
||||
# Get latest commit info
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--pretty=format:%H|%s|%an"],
|
||||
cwd=PIXELLE_PATH,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
commit_hash, commit_msg, author = result.stdout.split("|")
|
||||
# Get diff (mocking generic diff)
|
||||
diff_result = subprocess.run(
|
||||
["git", "show", commit_hash],
|
||||
cwd=PIXELLE_PATH,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
diff = diff_result.stdout[:2000] # Limit diff size
|
||||
|
||||
payload = {
|
||||
"commits": [
|
||||
{
|
||||
"id": commit_hash,
|
||||
"message": f"{commit_msg} #PV-INIT", # Add fake task ID to trigger audit
|
||||
"author": {"name": author},
|
||||
"modified": ["various files"],
|
||||
"patch": diff
|
||||
}
|
||||
],
|
||||
"repository": {"name": "Pixelle-Video"}
|
||||
}
|
||||
|
||||
resp = await client.post(f"{API_BASE}/webhooks/git", json=payload)
|
||||
if resp.status_code == 200:
|
||||
print(f"Webhook Processed: {resp.json()}")
|
||||
else:
|
||||
print(f"Webhook Failed: {resp.text}")
|
||||
else:
|
||||
print("Failed to get git log from Pixelle-Video")
|
||||
except Exception as e:
|
||||
print(f"Error simulating webhook: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
#!/bin/bash
|
||||
# Neural Sync 启动器 - 一键启动前后端服务
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "🚀 Neural Sync 启动器"
|
||||
echo "===================="
|
||||
|
||||
# 颜色定义
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ ! -d "venv" ]; then
|
||||
echo -e "${RED}❌ 虚拟环境不存在,请先创建: python -m venv venv${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 清理端口
|
||||
echo "🧹 清理端口..."
|
||||
lsof -ti:8000 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
lsof -ti:5173 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 启动后端
|
||||
echo -e "${GREEN}▶️ 启动后端服务...${NC}"
|
||||
cd backend
|
||||
nohup uvicorn app.main:app --reload --port 8000 > ../logs/backend.log 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
cd ..
|
||||
|
||||
# 等待后端启动
|
||||
echo "⏳ 等待后端启动..."
|
||||
for i in {1..10}; do
|
||||
if curl -s http://127.0.0.1:8000/docs > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ 后端启动成功 (PID: $BACKEND_PID)${NC}"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 10 ]; then
|
||||
echo -e "${RED}❌ 后端启动失败,查看日志: logs/backend.log${NC}"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 启动前端
|
||||
echo -e "${GREEN}▶️ 启动前端服务...${NC}"
|
||||
cd frontend
|
||||
nohup npm run dev -- --host > ../logs/frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
cd ..
|
||||
|
||||
# 等待前端启动
|
||||
echo "⏳ 等待前端启动..."
|
||||
for i in {1..15}; do
|
||||
if curl -s http://127.0.0.1:5173 > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ 前端启动成功 (PID: $FRONTEND_PID)${NC}"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 15 ]; then
|
||||
echo -e "${YELLOW}⚠️ 前端可能还在启动中,查看日志: logs/frontend.log${NC}"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 保存 PID 到文件
|
||||
echo "$BACKEND_PID" > logs/backend.pid
|
||||
echo "$FRONTEND_PID" > logs/frontend.pid
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 所有服务启动完成!${NC}"
|
||||
echo "===================="
|
||||
echo -e " 后端: ${YELLOW}http://127.0.0.1:8000${NC}"
|
||||
echo -e " 前端: ${YELLOW}http://127.0.0.1:5173${NC}"
|
||||
echo -e " API 文档: ${YELLOW}http://127.0.0.1:8000/docs${NC}"
|
||||
echo ""
|
||||
echo "📝 日志文件:"
|
||||
echo " 后端: logs/backend.log"
|
||||
echo " 前端: logs/frontend.log"
|
||||
echo ""
|
||||
echo -e "${YELLOW}💡 停止服务: ./stop.sh${NC}"
|
||||
echo -e "${YELLOW}💡 查看日志: tail -f logs/backend.log 或 logs/frontend.log${NC}"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Neural Sync 停止器
|
||||
停止所有服务
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# 颜色输出
|
||||
class Colors:
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
NC = '\033[0m'
|
||||
|
||||
def print_colored(color, text):
|
||||
print(f"{color}{text}{Colors.NC}")
|
||||
|
||||
def get_project_dir():
|
||||
return Path(__file__).parent
|
||||
|
||||
def kill_port(port):
|
||||
"""终止占用指定端口的进程"""
|
||||
system = platform.system()
|
||||
try:
|
||||
if system == "Darwin": # macOS
|
||||
result = subprocess.run(["lsof", "-ti", f"{port}"],
|
||||
capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
subprocess.run(["kill", "-9"] + result.stdout.strip().split(),
|
||||
capture_output=True)
|
||||
return True
|
||||
elif system == "Linux":
|
||||
subprocess.run(["fuser", "-k", f"{port}/tcp"],
|
||||
capture_output=True)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def stop_by_pid(pid_file):
|
||||
"""通过 PID 文件停止进程"""
|
||||
pid_file_path = get_project_dir() / "logs" / pid_file
|
||||
if pid_file_path.exists():
|
||||
pid = int(pid_file_path.read_text().strip())
|
||||
try:
|
||||
os.kill(pid, 9) # SIGKILL
|
||||
print_colored(Colors.GREEN, f"✅ 进程已停止 (PID: {pid})")
|
||||
pid_file_path.unlink()
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
pid_file_path.unlink()
|
||||
return False
|
||||
return False
|
||||
|
||||
def main():
|
||||
print_colored(Colors.YELLOW, "🛑 停止 Neural Sync 服务")
|
||||
print("=" * 30)
|
||||
|
||||
stopped = []
|
||||
|
||||
# 从 PID 文件停止
|
||||
if stop_by_pid("backend.pid"):
|
||||
stopped.append("后端")
|
||||
|
||||
if stop_by_pid("frontend.pid"):
|
||||
stopped.append("前端")
|
||||
|
||||
# 清理端口
|
||||
if kill_port(8000):
|
||||
stopped.append("后端端口")
|
||||
|
||||
if kill_port(5173):
|
||||
stopped.append("前端端口")
|
||||
|
||||
if stopped:
|
||||
print_colored(Colors.GREEN, f"🎉 已停止: {', '.join(stopped)}")
|
||||
else:
|
||||
print_colored(Colors.YELLOW, "⚠️ 没有找到运行中的服务")
|
||||
|
||||
# 询问是否停止 Ollama
|
||||
try:
|
||||
response = input("是否同时停止 Ollama? (y/N): ").strip().lower()
|
||||
if response == 'y':
|
||||
subprocess.run(["pkill", "-9", "-f", "ollama serve"],
|
||||
capture_output=True)
|
||||
print_colored(Colors.GREEN, "✅ Ollama 已停止")
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
#!/bin/bash
|
||||
# Neural Sync 停止器 - 停止所有服务
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "🛑 停止 Neural Sync 服务"
|
||||
echo "===================="
|
||||
|
||||
# 从 PID 文件读取并停止
|
||||
if [ -f "logs/backend.pid" ]; then
|
||||
BACKEND_PID=$(cat logs/backend.pid)
|
||||
if ps -p $BACKEND_PID > /dev/null 2>&1; then
|
||||
kill $BACKEND_PID 2>/dev/null
|
||||
echo -e "${GREEN}✅ 后端已停止 (PID: $BACKEND_PID)${NC}"
|
||||
fi
|
||||
rm logs/backend.pid
|
||||
fi
|
||||
|
||||
if [ -f "logs/frontend.pid" ]; then
|
||||
FRONTEND_PID=$(cat logs/frontend.pid)
|
||||
if ps -p $FRONTEND_PID > /dev/null 2>&1; then
|
||||
kill $FRONTEND_PID 2>/dev/null
|
||||
echo -e "${GREEN}✅ 前端已停止 (PID: $FRONTEND_PID)${NC}"
|
||||
fi
|
||||
rm logs/frontend.pid
|
||||
fi
|
||||
|
||||
# 清理端口
|
||||
lsof -ti:8000 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
lsof -ti:5173 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
|
||||
# 停止 Ollama(可选)
|
||||
read -p "是否同时停止 Ollama? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
pkill -9 -f "ollama serve" 2>/dev/null && echo -e "${GREEN}✅ Ollama 已停止${NC}" || true
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🎉 所有服务已停止${NC}"
|
||||
|
|
@ -0,0 +1,570 @@
|
|||
# Neural Sync - 用户使用手册
|
||||
|
||||
> **面向 AI 技术团队的智能项目管理平台**
|
||||
> 版本:v1.0
|
||||
> 更新日期:2024年2月
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统简介](#1-系统简介)
|
||||
2. [快速开始](#2-快速开始)
|
||||
3. [核心功能使用](#3-核心功能使用)
|
||||
4. [日常使用流程](#4-日常使用流程)
|
||||
5. [系统配置](#5-系统配置)
|
||||
6. [常见问题](#6-常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统简介
|
||||
|
||||
### 1.1 什么是 Neural Sync?
|
||||
|
||||
**Neural Sync** 是一个专为 AI/ML 技术团队设计的项目管理平台,与传统项目管理工具不同,它专注于管理"活体上下文"——外部信号、AI 实验和认知知识节点。
|
||||
|
||||
### 1.2 核心价值
|
||||
|
||||
| 传统 PM 工具 | Neural Sync |
|
||||
|--------------|-------------|
|
||||
| 管理静态任务 | 管理动态知识 |
|
||||
| 人工优先级判断 | AI 自动战略研判 |
|
||||
| 离散的工具链 | 统一的 AI 工作流 |
|
||||
|
||||
### 1.3 三大核心功能
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 信号监控 实验管理 知识管理 │
|
||||
│ │ │ │
|
||||
│ 外部技术情报 AI 实验追踪 认知节点 │
|
||||
│ - HackerNews - 假设验证 - 版本控制 │
|
||||
│ - 行业动态 - 评分系统 - 向量检索 │
|
||||
│ - 竞品更新 - 关联任务 - AI 洞察提取 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 快速开始
|
||||
|
||||
### 2.1 安装部署
|
||||
|
||||
#### 前置要求
|
||||
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
- PostgreSQL 15+ (推荐) 或 SQLite (开发环境)
|
||||
|
||||
#### 步骤一:启动 Ollama AI 服务
|
||||
|
||||
```bash
|
||||
# 安装 Ollama (如果尚未安装)
|
||||
# macOS: brew install ollama
|
||||
# Linux: curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# 启动 Ollama 服务
|
||||
ollama serve
|
||||
|
||||
# 拉取所需模型 (新终端窗口)
|
||||
ollama pull llama3
|
||||
ollama pull nomic-embed-text
|
||||
```
|
||||
|
||||
#### 步骤二:启动后端服务
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 配置环境变量 (可选,使用默认配置)
|
||||
cp .env.example .env
|
||||
|
||||
# 运行数据库迁移
|
||||
alembic upgrade head
|
||||
|
||||
# 启动后端
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
#### 步骤三:启动前端界面
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev -- --host
|
||||
```
|
||||
|
||||
#### 步骤四:访问系统
|
||||
|
||||
打开浏览器访问:`http://localhost:5173`
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心功能使用
|
||||
|
||||
### 3.1 信号监控
|
||||
|
||||
**用途**:捕获和分析外部技术信号,评估其对团队的战略影响。
|
||||
|
||||
#### 如何使用
|
||||
|
||||
1. **自动获取信号**
|
||||
|
||||
访问前端页面,系统会自动从 HackerNews 获取最新技术动态。
|
||||
|
||||
```bash
|
||||
# 手动触发信号获取
|
||||
curl -X POST http://localhost:8000/signals/fetch
|
||||
```
|
||||
|
||||
2. **手动添加信号**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/signals/ingest \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"source": "手动输入",
|
||||
"content": "OpenAI 发布了新的视频生成模型 Sora",
|
||||
"impact_score": 0.9
|
||||
}'
|
||||
```
|
||||
|
||||
3. **查看战略分析**
|
||||
|
||||
点击左侧信号列表中的任意信号,右侧将显示:
|
||||
- AI 战略研判结果
|
||||
- 置信度评分
|
||||
- 多模型投票情况
|
||||
- 相关知识引用
|
||||
- 竞争力雷达图
|
||||
|
||||
#### 优先级说明
|
||||
|
||||
系统会自动根据影响分数分配优先级:
|
||||
|
||||
| 优先级 | 图标 | 影响分数 | 处理方式 |
|
||||
|--------|------|----------|----------|
|
||||
| 🔴 紧急 | critical | ≥ 0.9 | 立即处理 + 通知 |
|
||||
| 🟠 高优 | high | 0.7-0.9 | 异步处理 + 通知 |
|
||||
| 🟡 中等 | medium | 0.5-0.7 | 异步处理 |
|
||||
| 🟢 低优 | low | < 0.5 | 仅记录 |
|
||||
|
||||
### 3.2 实验管理
|
||||
|
||||
**用途**:管理 AI 实验的科学方法,通过假设-验证循环驱动技术决策。
|
||||
|
||||
#### 如何使用
|
||||
|
||||
1. **创建新实验**
|
||||
|
||||
点击"新建实验"按钮,填写:
|
||||
- **标题**:实验名称
|
||||
- **假设**:例如"切换到 DeepSeek-V3 后,准确率预计提升 5%"
|
||||
- **目标指标**:accuracy、latency、cost 等
|
||||
- **关联知识**:选择相关的认知节点
|
||||
|
||||
2. **追踪进度**
|
||||
|
||||
- 实验状态:草稿 → 进行中 → 成功/失败
|
||||
- 当前评分:基于目标指标的实时评分
|
||||
- 状态自动更新:根据关联知识节点的变化自动标记"已过时"
|
||||
|
||||
3. **查看实验列表**
|
||||
|
||||
访问前端实验看板,查看所有进行中和已完成的实验。
|
||||
|
||||
### 3.3 知识管理
|
||||
|
||||
**用途**:管理团队的技术认知,作为 AI 战略研判的知识库。
|
||||
|
||||
#### 如何使用
|
||||
|
||||
1. **添加认知节点**
|
||||
|
||||
点击"新增认知",输入 Markdown 格式的知识内容:
|
||||
|
||||
```markdown
|
||||
# FLUX 图像生成模型
|
||||
|
||||
## 技术规格
|
||||
- 推理速度: ~8s/张
|
||||
- 分辨率: 1024x1024
|
||||
- 风格: FLUX.1-dev
|
||||
|
||||
## 核心优势
|
||||
- 无需复杂的 prompt 工程
|
||||
- 遵循能力强
|
||||
- 开源免费
|
||||
```
|
||||
|
||||
2. **AI 自动分析**
|
||||
|
||||
系统会自动:
|
||||
- 生成向量嵌入(用于语义检索)
|
||||
- 提取核心洞察(护城河、技术参数)
|
||||
- 计算技术栈关键词
|
||||
|
||||
3. **语义检索**
|
||||
|
||||
当新信号进入时,系统会自动找到最相关的知识节点辅助判断。
|
||||
|
||||
---
|
||||
|
||||
## 4. 日常使用流程
|
||||
|
||||
### 4.1 每日信号审查流程
|
||||
|
||||
```
|
||||
1. 早上打开系统 → 查看信号监控面板
|
||||
2. 点击高优先级信号 → 查看战略分析
|
||||
3. 对于战略级信号:
|
||||
- 查看 AI 建议的任务列表
|
||||
- 点击"授权下发"同步到 Plane
|
||||
- 团队接收 Lark 通知
|
||||
4. 提交反馈 → 帮助 AI 学习
|
||||
```
|
||||
|
||||
### 4.2 技术决策流程
|
||||
|
||||
```
|
||||
1. 外部技术变化(如新模型发布)
|
||||
↓
|
||||
2. 创建实验验证假设
|
||||
↓
|
||||
3. 关联相关知识节点
|
||||
↓
|
||||
4. 运行实验,收集评分数据
|
||||
↓
|
||||
5. 根据结果决定是否跟进
|
||||
```
|
||||
|
||||
### 4.3 知识沉淀流程
|
||||
|
||||
```
|
||||
1. 技术调研/架构决策
|
||||
↓
|
||||
2. 添加到认知节点
|
||||
↓
|
||||
3. AI 自动提取洞察
|
||||
↓
|
||||
4. 成为后续研判的知识基础
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 系统配置
|
||||
|
||||
### 5.1 环境变量配置
|
||||
|
||||
编辑 `backend/.env` 文件:
|
||||
|
||||
```bash
|
||||
# 数据库配置
|
||||
DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db
|
||||
|
||||
# Ollama AI 服务
|
||||
OLLAMA_HOST=http://127.0.0.1:11434
|
||||
|
||||
# 业务上下文(AI 战略研判的基础)
|
||||
BUSINESS_CONTEXT=你的产品/团队描述
|
||||
|
||||
# 多模型投票配置
|
||||
VOTING_MODELS=llama3,qwen2.5,mistral
|
||||
VOTING_THRESHOLD=0.8
|
||||
|
||||
# Plane PM 工具集成(可选)
|
||||
PLANE_API_URL=
|
||||
PLANE_API_KEY=
|
||||
PLANE_WORKSPACE_SLUG=
|
||||
PLANE_PROJECT_ID=
|
||||
|
||||
# Lark 飞书集成(可选)
|
||||
LARK_APP_ID=
|
||||
LARK_APP_SECRET=
|
||||
LARK_VERIFICATION_TOKEN=
|
||||
```
|
||||
|
||||
### 5.2 业务上下文配置
|
||||
|
||||
**BUSINESS_CONTEXT** 是 AI 战略研判的核心基础,应根据实际情况配置:
|
||||
|
||||
```bash
|
||||
# 示例:短视频生成平台
|
||||
BUSINESS_CONTEXT="我们是一个 AI 短视频自动生成平台。核心功能:输入主题→AI 生成脚本→AI 生成图片→语音合成→视频合成。技术栈:Python, Vue, FastAPI, ComfyUI。"
|
||||
|
||||
# 示例:AI 漫剧平台
|
||||
BUSINESS_CONTEXT="我们是一个 AI 漫剧创作平台。核心功能:漫画转视频、角色一致性、多语言翻译。技术栈:Stable Diffusion, RAG, Llama 3。"
|
||||
```
|
||||
|
||||
### 5.3 Ollama 模型管理
|
||||
|
||||
查看已安装模型:
|
||||
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
下载新模型:
|
||||
|
||||
```bash
|
||||
ollama pull qwen2.5 # 通用模型
|
||||
ollama pull mistral # 代码分析模型
|
||||
ollama pull deepseek-coder # 编程专用模型
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### 6.1 后端启动失败
|
||||
|
||||
**问题**:`ModuleNotFoundError: No module named 'xxx'`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 6.2 数据库连接失败
|
||||
|
||||
**问题**:`could not connect to server`
|
||||
|
||||
**解决方案 A - 使用 SQLite(开发环境)**:
|
||||
|
||||
编辑 `backend/.env`:
|
||||
```bash
|
||||
DATABASE_URL=sqlite+aiosqlite:///./neural_sync.db
|
||||
```
|
||||
|
||||
**解决方案 B - PostgreSQL 配置**:
|
||||
|
||||
```bash
|
||||
# 启动 PostgreSQL
|
||||
brew services start postgresql # macOS
|
||||
sudo systemctl start postgresql # Linux
|
||||
|
||||
# 创建数据库
|
||||
createdb neural_sync
|
||||
psql neural_sync -c "CREATE EXTENSION vector;"
|
||||
```
|
||||
|
||||
### 6.3 Ollama 连接失败
|
||||
|
||||
**问题**:AI 战略分析没有返回结果
|
||||
|
||||
**检查步骤**:
|
||||
|
||||
```bash
|
||||
# 1. 确认 Ollama 服务运行
|
||||
ps aux | grep ollama
|
||||
|
||||
# 2. 测试连接
|
||||
curl http://127.0.0.1:11434/api/tags
|
||||
|
||||
# 3. 测试模型
|
||||
ollama run llama3 "测试"
|
||||
```
|
||||
|
||||
### 6.4 前端无法连接后端
|
||||
|
||||
**检查配置**:
|
||||
|
||||
1. 确认后端运行在 `http://localhost:8000`
|
||||
2. 检查浏览器控制台的网络请求
|
||||
3. 确认没有 CORS 错误
|
||||
|
||||
### 6.5 向量相似度搜索不工作
|
||||
|
||||
**问题**:RAG 检索没有返回相关知识
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 确保认知节点有嵌入:
|
||||
```bash
|
||||
curl http://localhost:8000/context_nodes/
|
||||
```
|
||||
|
||||
2. 检查嵌入是否生成:
|
||||
```python
|
||||
# 在 Python 中测试
|
||||
from app.services.llm import llm_service
|
||||
vec = llm_service.get_embedding("测试文本")
|
||||
print(len(vec)) # 应该是 768
|
||||
```
|
||||
|
||||
### 6.6 Plane 同步失败
|
||||
|
||||
**问题**:任务没有同步到 Plane
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 检查 API 配置是否正确
|
||||
2. 如果没有配置 API Key,系统运行在模拟模式(仅打印日志)
|
||||
3. 查看后端日志确认同步状态
|
||||
|
||||
### 6.7 多模型投票不生效
|
||||
|
||||
**问题**:高影响信号没有使用多模型投票
|
||||
|
||||
**检查**:
|
||||
|
||||
1. 确认影响分数 >= 0.8
|
||||
2. 确认模型已安装:
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
3. 检查环境变量配置:
|
||||
```bash
|
||||
VOTING_MODELS=llama3,qwen2.5,mistral
|
||||
VOTING_THRESHOLD=0.8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API 参考
|
||||
|
||||
### 7.1 信号相关
|
||||
|
||||
```bash
|
||||
# 获取所有信号
|
||||
GET /signals/
|
||||
|
||||
# 手动添加信号
|
||||
POST /signals/ingest
|
||||
Body: {
|
||||
"source": "HackerNews",
|
||||
"content": "OpenAI 发布 Sora",
|
||||
"impact_score": 0.95
|
||||
}
|
||||
|
||||
# 触发信号获取
|
||||
POST /signals/fetch
|
||||
```
|
||||
|
||||
### 7.2 实验相关
|
||||
|
||||
```bash
|
||||
# 获取所有实验
|
||||
GET /experiments/
|
||||
|
||||
# 创建实验
|
||||
POST /experiments/
|
||||
Body: {
|
||||
"title": "FLUX 图像生成测试",
|
||||
"hypothesis": "FLUX 比 SDXL 快 2 倍",
|
||||
"target_metric": "latency",
|
||||
"linked_context_nodes": ["uuid-1", "uuid-2"]
|
||||
}
|
||||
|
||||
# 更新实验评分
|
||||
PATCH /experiments/{id}
|
||||
Body: {
|
||||
"current_score": 8.5
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 知识节点相关
|
||||
|
||||
```bash
|
||||
# 获取所有认知节点
|
||||
GET /context_nodes/
|
||||
|
||||
# 创建认知节点
|
||||
POST /context_nodes/
|
||||
Body: {
|
||||
"content": "# FLUX 模型\n\n技术规格..."
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 反馈相关
|
||||
|
||||
```bash
|
||||
# 提交反馈
|
||||
POST /signals/{signal_id}/feedback
|
||||
Body: {
|
||||
"was_strategic": true,
|
||||
"reason": "判断准确,确实需要跟进",
|
||||
"user_id": "user@example.com"
|
||||
}
|
||||
|
||||
# 获取反馈统计
|
||||
GET /feedbacks/stats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 最佳实践
|
||||
|
||||
### 8.1 信号管理
|
||||
|
||||
- **定期审查**:每天查看高优先级信号
|
||||
- **及时反馈**:帮助 AI 系统学习
|
||||
- **关注置信度**:低置信度的判断需要人工复核
|
||||
|
||||
### 8.2 实验管理
|
||||
|
||||
- **假设要具体**:避免模糊的假设
|
||||
- **指标可量化**:使用可测量的指标
|
||||
- **关联知识**:链接相关认知节点
|
||||
|
||||
### 8.3 知识管理
|
||||
|
||||
- **定期更新**:技术变化时及时更新节点
|
||||
- **结构化格式**:使用 Markdown 标题和列表
|
||||
- **包含技术规格**:帮助 AI 理解技术细节
|
||||
|
||||
---
|
||||
|
||||
## 9. 技术支持
|
||||
|
||||
如有问题,请联系技术团队或查看:
|
||||
- 后端日志:`backend/app/main.py` 输出
|
||||
- 前端日志:浏览器控制台
|
||||
- API 文档:`http://localhost:8000/docs`
|
||||
|
||||
---
|
||||
|
||||
## 附录:项目结构
|
||||
|
||||
```
|
||||
Neural Sync/
|
||||
├── backend/ # Python 后端
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI 主应用
|
||||
│ │ ├── models.py # 数据模型
|
||||
│ │ ├── database.py # 数据库连接
|
||||
│ │ └── services/ # 业务逻辑
|
||||
│ ├── alembic/ # 数据库迁移
|
||||
│ ├── requirements.txt
|
||||
│ └── .env.example # 环境变量模板
|
||||
│
|
||||
├── frontend/ # Vue 3 前端
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Vue 组件
|
||||
│ │ ├── App.vue # 主应用
|
||||
│ │ └── main.ts # 入口文件
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
│
|
||||
├── CLAUDE.md # Claude Code 指导
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2024年2月19日
|
||||
Loading…
Reference in New Issue