Compare commits
43 Commits
e049e4e104
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 737a80aa39 | |||
| bdd4ddec9e | |||
| 2ee55d25cc | |||
| bf9ce709e2 | |||
| 59331d2435 | |||
| a9488a655a | |||
| eeadd4c825 | |||
| 42d8b8e8e1 | |||
| da6cc1e18b | |||
| 691ef1c77d | |||
| 9f9813f01d | |||
| f9f3651c7e | |||
| 87407e1656 | |||
| 2c226e2df2 | |||
| 8b2a506177 | |||
| a26cda2f04 | |||
| 5e2a86e3be | |||
| 2c7d5ea589 | |||
| 36e3b40936 | |||
| 86f6e4f81c | |||
| 60f3029e54 | |||
| fe7ff9a516 | |||
| 33faedc1b1 | |||
| c9b1c5cb32 | |||
| 8f99d47af9 | |||
| 1d36f196ca | |||
| 9f40ef93b7 | |||
| fda3e86a71 | |||
| d5303ad201 | |||
| 245be63f07 | |||
| 068df7715e | |||
| 95498180a2 | |||
| 7b6ba79417 | |||
| a228325d74 | |||
| e59ae66b85 | |||
| e524be15e5 | |||
| 2d90a4caac | |||
| 4e8c2b4f5d | |||
| 0ddeb91755 | |||
| cb3b98db10 | |||
| 487562042f | |||
| ac46518cf5 | |||
| c03fb17a5c |
30
README.md
30
README.md
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
这是一个基于FastAPI的聊天API服务,使用OpenAI格式的请求来调用pipeline.invoke方法进行聊天。
|
这是一个基于FastAPI的聊天API服务,使用OpenAI格式的请求来调用pipeline.invoke方法进行聊天。
|
||||||
|
|
||||||
|
## Docker Installation
|
||||||
|
|
||||||
|
For production deployment using Docker, see the [Installation Guide](README_INSTALL.md).
|
||||||
|
|
||||||
## 安装依赖
|
## 安装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -140,15 +144,35 @@ npm install
|
|||||||
|
|
||||||
### Start the `front_apis` server
|
### Start the `front_apis` server
|
||||||
|
|
||||||
The frontend talks to the `front_apis` FastAPI service, which by default listens on `http://127.0.0.1:8001`.
|
The frontend talks to the `front_apis` FastAPI service, which by default listens on `http://127.0.0.1:8500`.
|
||||||
|
|
||||||
From the project root:
|
From the project root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvicorn fastapi_server.front_apis:app --reload --host 0.0.0.0 --port 8001
|
uvicorn fastapi_server.front_apis:app --reload --host 0.0.0.0 --port 8500
|
||||||
```
|
```
|
||||||
|
|
||||||
You can change the URL by setting `VITE_FRONT_API_BASE_URL` in `frontend/.env` (defaults to `http://127.0.0.1:8001`).
|
Or run directly:
|
||||||
|
```bash
|
||||||
|
python fastapi_server/front_apis.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend run modes
|
||||||
|
|
||||||
|
Run whichever backend mode you need from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# admin/control plane only (/v1/... frontend APIs)
|
||||||
|
uvicorn fastapi_server.front_apis:app --reload --host 0.0.0.0 --port 8500
|
||||||
|
|
||||||
|
# DashScope chat runtime only (/apps/... and /v1/apps/... APIs)
|
||||||
|
uvicorn fastapi_server.server_dashscope:app --reload --host 0.0.0.0 --port 8588
|
||||||
|
|
||||||
|
# combined mode: one process serves both front_apis + DashScope endpoints
|
||||||
|
uvicorn fastapi_server.combined:app --reload --host 0.0.0.0 --port 8500
|
||||||
|
```
|
||||||
|
|
||||||
|
You can change the URL by setting `VITE_FRONT_API_BASE_URL` in `frontend/.env` (defaults to `/`, i.e. same-origin).
|
||||||
|
|
||||||
### Start the development server
|
### Start the development server
|
||||||
|
|
||||||
|
|||||||
267
README_INSTALL.md
Normal file
267
README_INSTALL.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Installation Guide
|
||||||
|
|
||||||
|
This guide explains how to install and run the LangChain Agent application using Docker.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker (version 20.10 or later)
|
||||||
|
- Docker Compose (version 2.0 or later, or use `docker compose` command)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Run the installation script:**
|
||||||
|
```bash
|
||||||
|
./scripts/shell_scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
- Check for required tools (Docker, docker-compose)
|
||||||
|
- Create a `.env` file with default configuration
|
||||||
|
- Build Docker images (or use pre-loaded images)
|
||||||
|
- Start all services (PostgreSQL, Backend API, Nginx)
|
||||||
|
|
||||||
|
2. **Access the application:**
|
||||||
|
- Frontend: http://localhost (or http://localhost:80)
|
||||||
|
- Backend API: http://localhost:8500
|
||||||
|
- Database: localhost:5432
|
||||||
|
|
||||||
|
## Installation for China / Offline Use
|
||||||
|
|
||||||
|
If Docker Hub is slow or inaccessible in your region:
|
||||||
|
|
||||||
|
### Option 1: Use Chinese Docker Mirrors
|
||||||
|
|
||||||
|
Configure Docker to use Chinese registry mirrors:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/docker/daemon.json <<EOF
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://registry.docker-cn.com",
|
||||||
|
"https://mirror.ccsogou.com",
|
||||||
|
"https://docker.1ms.run"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `./scripts/shell_scripts/install.sh`
|
||||||
|
|
||||||
|
### configuring '.env' and 'frontend/.env'
|
||||||
|
```bash
|
||||||
|
ALI_API_KEY="API_KEY_FOR_ALI_QWEN"
|
||||||
|
ALI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
|
POSTGRES_ROOT_PASSWORD="ROOT_PASSOWRD_FOR_DB_IN_CONN_STR - required for installation"
|
||||||
|
POSTGRES_PASSWORD="USER_PASSWORD_FOR_DB_CONN_STR - required for installation" - need to be same in POSTGRES_PASSWORD
|
||||||
|
CONN_STR="CONNECTION_STRING_TO_DATABASE" # DOCKER PASSWORD
|
||||||
|
FAST_AUTH_KEYS="API_KEY_FOR_OTHER_APPLICATIONS_TO_USE_BUILT_PIPELINE"
|
||||||
|
DAYTONA_API_KEY="DAYTONA_CONFIG - NOT REQUIRED"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Pre-load Docker Images Offline
|
||||||
|
|
||||||
|
1. On a machine with good Docker Hub access, run:
|
||||||
|
```bash
|
||||||
|
./scripts/shell_scripts/download_images.sh
|
||||||
|
```
|
||||||
|
This creates `images.tar` with all required images.
|
||||||
|
|
||||||
|
2. Transfer `images.tar` to your target machine.
|
||||||
|
|
||||||
|
3. Load the images:
|
||||||
|
```bash
|
||||||
|
docker load < images.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the install script:
|
||||||
|
```bash
|
||||||
|
./scripts/shell_scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
If you prefer to set up manually:
|
||||||
|
|
||||||
|
1. **Create environment file:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # Edit as needed
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build and start services:**
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check service status:**
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit the `.env` file to customize:
|
||||||
|
|
||||||
|
- `POSTGRES_DB`: Database name (default: `ai_conversations`)
|
||||||
|
- `POSTGRES_USER`: Database user (default: `myapp_user`)
|
||||||
|
- `POSTGRES_PASSWORD`: Database password (default: `secure_password_123`)
|
||||||
|
- `POSTGRES_PORT`: PostgreSQL port (default: `5432`)
|
||||||
|
- `BACKEND_PORT`: Backend API port (default: `8500`)
|
||||||
|
- `FRONTEND_PORT`: Frontend web server port (default: `80`)
|
||||||
|
|
||||||
|
## Database Initialization
|
||||||
|
|
||||||
|
The database is automatically initialized when the PostgreSQL container starts for the first time. The following SQL scripts are executed in order:
|
||||||
|
|
||||||
|
1. `scripts/init_database/00_init_user.sh` - Creates database user and database
|
||||||
|
2. `scripts/init_database/create_conv_store.sql` - Creates conversation storage tables
|
||||||
|
3. `scripts/init_database/create_prompt_config.sql` - Creates prompt configuration tables
|
||||||
|
|
||||||
|
## Service Management
|
||||||
|
|
||||||
|
All commands run from the `docker/` directory:
|
||||||
|
|
||||||
|
### View logs:
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f backend
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f postgres
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop services:
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart services:
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild after code changes:
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset database (delete all data):
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml down -v
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The application consists of three main services:
|
||||||
|
|
||||||
|
1. **PostgreSQL** (`postgres`): Database server
|
||||||
|
- Stores conversations and prompt configurations
|
||||||
|
- Automatically initializes schema on first run
|
||||||
|
|
||||||
|
2. **Backend** (`backend`): FastAPI application
|
||||||
|
- Serves API endpoints at port 8500
|
||||||
|
- Handles agent management and chat endpoints
|
||||||
|
- Connects to PostgreSQL database
|
||||||
|
|
||||||
|
3. **Nginx** (`nginx`): Web server
|
||||||
|
- Serves the React frontend (port 80)
|
||||||
|
- Proxies API requests to the backend
|
||||||
|
- Handles static file serving
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
langchain-agent/
|
||||||
|
├── docker/
|
||||||
|
│ ├── docker-compose.prod.yml # Production compose file
|
||||||
|
│ └── Dockerfile.prod # Backend Docker image
|
||||||
|
├── scripts/
|
||||||
|
│ ├── shell_scripts/
|
||||||
|
│ │ ├── install.sh # Main installation script
|
||||||
|
│ │ └── download_images.sh # For offline image download
|
||||||
|
│ └── init_database/ # Database initialization scripts
|
||||||
|
├── frontend/ # React frontend
|
||||||
|
├── configs/ # Pipeline configurations
|
||||||
|
├── nginx.conf # Nginx configuration
|
||||||
|
└── .env # Environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Database connection issues
|
||||||
|
|
||||||
|
If the backend can't connect to the database:
|
||||||
|
|
||||||
|
1. Check that PostgreSQL is running:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml ps postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify the connection string in `.env` matches the database configuration
|
||||||
|
|
||||||
|
3. Check backend logs:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml logs backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend not loading / NetworkError
|
||||||
|
|
||||||
|
1. Check nginx logs:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml logs nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure frontend is built with correct API base URL. The `frontend/.env` file should contain:
|
||||||
|
```
|
||||||
|
VITE_FRONT_API_BASE_URL=/
|
||||||
|
```
|
||||||
|
Then rebuild: `docker compose -f docker-compose.prod.yml build backend`
|
||||||
|
|
||||||
|
### Port conflicts
|
||||||
|
|
||||||
|
If ports are already in use, update the port mappings in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: use port 5433 for PostgreSQL
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For development, you may want to run services separately:
|
||||||
|
|
||||||
|
1. Start only PostgreSQL:
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
docker compose -f docker-compose.prod.yml up -d postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run backend locally:
|
||||||
|
```bash
|
||||||
|
export CONN_STR="postgresql://myapp_user:secure_password_123@localhost:5432/ai_conversations"
|
||||||
|
python -m uvicorn lang_agent.fastapi_server.combined:app --reload --host 0.0.0.0 --port 8500
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run frontend locally:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: For local frontend development, create a `.env` file in `frontend/` with:
|
||||||
|
```
|
||||||
|
VITE_FRONT_API_BASE_URL=http://localhost:8500
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
you are a helpful bot enhanced with skills.
|
You are a helpful bot enhanced with skills.
|
||||||
|
|
||||||
To use a skill, read its SKILL.md file using the read_file tool. Skills are NOT tools — they are instructions for using existing tools.
|
To use a skill, read its SKILL.md file using the read_file tool. Skills are NOT tools — they are instructions for using existing tools.
|
||||||
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew. You can check if the environment the packages you need.
|
|
||||||
|
When using a skill, assume required tools (e.g., npx, curl) are available and execute the commands directly. If a command fails because a tool is missing, install the missing dependency using apt/brew and retry.
|
||||||
|
|
||||||
|
For shell commands (e.g., npx, curl), use the execute tool to run them.
|
||||||
19
docker/Dockerfile.frontend
Normal file
19
docker/Dockerfile.frontend
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# Build-time API base for Vite (must be set before npm run build).
|
||||||
|
ARG VITE_FRONT_API_BASE_URL=/
|
||||||
|
ENV VITE_FRONT_API_BASE_URL=${VITE_FRONT_API_BASE_URL}
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build && \
|
||||||
|
mkdir -p /opt/frontend_dist && \
|
||||||
|
cp -r dist/. /opt/frontend_dist/
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "rm -rf /app/dist/* && cp -r /opt/frontend_dist/. /app/dist && ls /app/dist"]
|
||||||
93
docker/Dockerfile.prod
Normal file
93
docker/Dockerfile.prod
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Multi-stage Dockerfile for production deployment
|
||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:20-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
|
# Copy frontend files
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Python backend
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN set -eux; \
|
||||||
|
for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do \
|
||||||
|
if [ -f "$source_file" ]; then \
|
||||||
|
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' "$source_file"; \
|
||||||
|
sed -i 's|security.debian.org|mirrors.aliyun.com|g' "$source_file"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
postgresql-client \
|
||||||
|
curl; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy Python dependencies
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple && \
|
||||||
|
pip install --no-cache-dir -e . -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY lang_agent/ ./lang_agent/
|
||||||
|
COPY configs/ ./configs/
|
||||||
|
COPY scripts/ ./scripts/
|
||||||
|
COPY assets/ ./assets/
|
||||||
|
COPY static/ ./static/
|
||||||
|
|
||||||
|
# Copy built frontend from stage 1
|
||||||
|
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8500
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8500/health || exit 1
|
||||||
|
|
||||||
|
# Create entrypoint script to wait for DB
|
||||||
|
# Uses Python to check database connection (more reliable than psql)
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
set -e\n\
|
||||||
|
echo "Waiting for database to be ready..."\n\
|
||||||
|
python3 << EOF\n\
|
||||||
|
import sys\n\
|
||||||
|
import time\n\
|
||||||
|
import psycopg\n\
|
||||||
|
\n\
|
||||||
|
max_attempts = 30\n\
|
||||||
|
conn_str = "${CONN_STR}"\n\
|
||||||
|
\n\
|
||||||
|
for i in range(max_attempts):\n\
|
||||||
|
try:\n\
|
||||||
|
with psycopg.connect(conn_str, connect_timeout=2) as conn:\n\
|
||||||
|
with conn.cursor() as cur:\n\
|
||||||
|
cur.execute("SELECT 1")\n\
|
||||||
|
print("Database is ready!")\n\
|
||||||
|
sys.exit(0)\n\
|
||||||
|
except Exception as e:\n\
|
||||||
|
if i == max_attempts - 1:\n\
|
||||||
|
print(f"Warning: Database not ready after {max_attempts * 2} seconds, continuing anyway...")\n\
|
||||||
|
print(f"Error: {e}")\n\
|
||||||
|
sys.exit(0)\n\
|
||||||
|
print(f"Database is unavailable - sleeping (attempt {i+1}/{max_attempts})")\n\
|
||||||
|
time.sleep(2)\n\
|
||||||
|
EOF\n\
|
||||||
|
exec "$@"' > /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
|
# Run the combined server
|
||||||
|
CMD ["python", "-m", "uvicorn", "lang_agent.fastapi_server.combined:app", "--host", "0.0.0.0", "--port", "8500"]
|
||||||
|
|
||||||
100
docker/docker-compose.prod.yml
Normal file
100
docker/docker-compose.prod.yml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL database
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: langchain-agent-db
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_ROOT_PASSWORD:-postgres_root_password}
|
||||||
|
# These are used by init scripts to create the app database
|
||||||
|
APP_DB_NAME: ${POSTGRES_DB:-ai_conversations}
|
||||||
|
APP_DB_USER: ${POSTGRES_USER:-myapp_user}
|
||||||
|
APP_DB_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ../scripts/init_database:/docker-entrypoint-initdb.d
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5434}:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: no #unless-stopped
|
||||||
|
|
||||||
|
# Backend API server
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.prod
|
||||||
|
container_name: langchain-agent-backend
|
||||||
|
environment:
|
||||||
|
- PYTHONPATH=/app
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- CONN_STR=postgresql://${POSTGRES_USER:-myapp_user}:${POSTGRES_PASSWORD:-secure_password_123}@postgres:5432/${POSTGRES_DB:-ai_conversations}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:-myapp_user}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-secure_password_123}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:-ai_conversations}
|
||||||
|
ports:
|
||||||
|
- "${BACKEND_PORT:-8500}:8500"
|
||||||
|
volumes:
|
||||||
|
- ../configs:/app/configs
|
||||||
|
- ../scripts:/app/scripts
|
||||||
|
- ../assets:/app/assets
|
||||||
|
- ../static:/app/static
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: no #unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8500/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Frontend build service
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ../frontend
|
||||||
|
dockerfile: ../docker/Dockerfile.frontend
|
||||||
|
args:
|
||||||
|
VITE_FRONT_API_BASE_URL: ${VITE_FRONT_API_BASE_URL:-/}
|
||||||
|
volumes:
|
||||||
|
- frontend_dist:/app/dist
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
# Nginx for serving frontend (optional - can also serve via FastAPI)
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: langchain-agent-nginx
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-8080}:80"
|
||||||
|
volumes:
|
||||||
|
- ../nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- frontend_dist:/usr/share/nginx/html:ro
|
||||||
|
depends_on:
|
||||||
|
frontend:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
backend:
|
||||||
|
condition: service_started
|
||||||
|
restart: no #unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
frontend_dist:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
BIN
frontend/assets/images/graph_arch/deepagent.png
Normal file
BIN
frontend/assets/images/graph_arch/deepagent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -36,20 +36,29 @@ type EditableAgent = {
|
|||||||
pipelineId: string;
|
pipelineId: string;
|
||||||
promptSetId?: string;
|
promptSetId?: string;
|
||||||
toolKeys: string[];
|
toolKeys: string[];
|
||||||
|
toolKeysInput: string;
|
||||||
prompts: Record<string, string>;
|
prompts: Record<string, string>;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
llmName: string;
|
llmName: string;
|
||||||
actBackend: DeepAgentActBackend;
|
actBackend: DeepAgentActBackend;
|
||||||
|
fileBackendConfig: FileBackendConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AgentChatMessage = {
|
type AgentChatMessage = {
|
||||||
id: string;
|
id: string;
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant" | "tool";
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActiveTab = "agents" | "discussions" | "mcp";
|
type ActiveTab = "agents" | "discussions" | "mcp";
|
||||||
type DeepAgentActBackend = "state_bk" | "local_shell" | "daytona_sandbox";
|
type DeepAgentActBackend = "state_bk" | "local_shell" | "daytona_sandbox";
|
||||||
|
|
||||||
|
type FileBackendConfig = {
|
||||||
|
skills_dir: string;
|
||||||
|
rt_skills_dir: string;
|
||||||
|
workspace_dir?: string;
|
||||||
|
api_key?: string;
|
||||||
|
};
|
||||||
type McpTransport = "streamable_http" | "sse" | "stdio";
|
type McpTransport = "streamable_http" | "sse" | "stdio";
|
||||||
type McpEntry = {
|
type McpEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -73,7 +82,24 @@ const DEEPAGENT_BACKEND_OPTIONS: Array<{
|
|||||||
{ value: "local_shell", label: "local_shell" },
|
{ value: "local_shell", label: "local_shell" },
|
||||||
{ value: "daytona_sandbox", label: "daytona_sandbox" },
|
{ value: "daytona_sandbox", label: "daytona_sandbox" },
|
||||||
];
|
];
|
||||||
const LOCAL_DASHSCOPE_BASE = "http://127.0.0.1:8500/v1/apps";
|
|
||||||
|
const DEFAULT_FILE_BACKEND_CONFIG: Record<DeepAgentActBackend, FileBackendConfig> = {
|
||||||
|
state_bk: {
|
||||||
|
skills_dir: "./assets/skills",
|
||||||
|
rt_skills_dir: "/skills",
|
||||||
|
},
|
||||||
|
local_shell: {
|
||||||
|
skills_dir: "./workspace/skills",
|
||||||
|
rt_skills_dir: "/skills",
|
||||||
|
workspace_dir: "./workspace",
|
||||||
|
},
|
||||||
|
daytona_sandbox: {
|
||||||
|
skills_dir: "./workspace/skills",
|
||||||
|
rt_skills_dir: "",
|
||||||
|
api_key: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const LOCAL_DASHSCOPE_BASE = "/v1/apps";
|
||||||
const MCP_TRANSPORT_OPTIONS: McpTransport[] = ["streamable_http", "sse", "stdio"];
|
const MCP_TRANSPORT_OPTIONS: McpTransport[] = ["streamable_http", "sse", "stdio"];
|
||||||
const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob(
|
const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob(
|
||||||
"../assets/images/graph_arch/*.{png,jpg,jpeg,webp,gif}",
|
"../assets/images/graph_arch/*.{png,jpg,jpeg,webp,gif}",
|
||||||
@@ -426,6 +452,28 @@ function createConversationId(): string {
|
|||||||
return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapConversationMessageToAgentChatMessage(
|
||||||
|
message: ConversationMessageItem
|
||||||
|
): AgentChatMessage | null {
|
||||||
|
const type = (message.message_type || "").toLowerCase();
|
||||||
|
let role: AgentChatMessage["role"] | null = null;
|
||||||
|
if (type === "human" || type === "user") {
|
||||||
|
role = "user";
|
||||||
|
} else if (type === "ai" || type === "assistant") {
|
||||||
|
role = "assistant";
|
||||||
|
} else if (type === "tool") {
|
||||||
|
role = "tool";
|
||||||
|
}
|
||||||
|
if (!role) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `${message.sequence_number}-${message.created_at}-${role}`,
|
||||||
|
role,
|
||||||
|
content: message.content || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
|
function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
|
||||||
if (value === "local_shell" || value === "localshell") {
|
if (value === "local_shell" || value === "localshell") {
|
||||||
return "local_shell";
|
return "local_shell";
|
||||||
@@ -439,9 +487,68 @@ function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
|
|||||||
return DEFAULT_DEEPAGENT_ACT_BACKEND;
|
return DEFAULT_DEEPAGENT_ACT_BACKEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultFileBackendConfig(
|
||||||
|
backend: DeepAgentActBackend
|
||||||
|
): FileBackendConfig {
|
||||||
|
return { ...DEFAULT_FILE_BACKEND_CONFIG[backend] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalString(
|
||||||
|
record: Record<string, unknown>,
|
||||||
|
key: keyof FileBackendConfig
|
||||||
|
): string | undefined {
|
||||||
|
const value = record[key];
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeepAgentEditorState(config: GraphConfigReadResponse): {
|
||||||
|
actBackend: DeepAgentActBackend;
|
||||||
|
fileBackendConfig: FileBackendConfig;
|
||||||
|
} {
|
||||||
|
const graphParams = isRecord(config.graph_params) ? config.graph_params : {};
|
||||||
|
const actBackend = normalizeDeepAgentActBackend(graphParams.act_bkend);
|
||||||
|
const defaults = getDefaultFileBackendConfig(actBackend);
|
||||||
|
const rawFileBackendConfig = isRecord(graphParams.file_backend_config)
|
||||||
|
? graphParams.file_backend_config
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!rawFileBackendConfig) {
|
||||||
|
return {
|
||||||
|
actBackend,
|
||||||
|
fileBackendConfig: defaults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
actBackend,
|
||||||
|
fileBackendConfig: {
|
||||||
|
...defaults,
|
||||||
|
...(readOptionalString(rawFileBackendConfig, "skills_dir") !== undefined
|
||||||
|
? { skills_dir: readOptionalString(rawFileBackendConfig, "skills_dir") as string }
|
||||||
|
: {}),
|
||||||
|
...(readOptionalString(rawFileBackendConfig, "rt_skills_dir") !== undefined
|
||||||
|
? { rt_skills_dir: readOptionalString(rawFileBackendConfig, "rt_skills_dir") as string }
|
||||||
|
: {}),
|
||||||
|
...(readOptionalString(rawFileBackendConfig, "workspace_dir") !== undefined
|
||||||
|
? { workspace_dir: readOptionalString(rawFileBackendConfig, "workspace_dir") as string }
|
||||||
|
: {}),
|
||||||
|
...(readOptionalString(rawFileBackendConfig, "api_key") !== undefined
|
||||||
|
? { api_key: readOptionalString(rawFileBackendConfig, "api_key") as string }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildGraphParams(editor: EditableAgent): Record<string, unknown> {
|
function buildGraphParams(editor: EditableAgent): Record<string, unknown> {
|
||||||
if (editor.graphId === "deepagent") {
|
if (editor.graphId === "deepagent") {
|
||||||
return { act_bkend: editor.actBackend };
|
return {
|
||||||
|
act_bkend: editor.actBackend,
|
||||||
|
file_backend_config: editor.fileBackendConfig,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -450,6 +557,7 @@ function toEditable(
|
|||||||
config: GraphConfigReadResponse,
|
config: GraphConfigReadResponse,
|
||||||
draft: boolean
|
draft: boolean
|
||||||
): EditableAgent {
|
): EditableAgent {
|
||||||
|
const deepAgentState = getDeepAgentEditorState(config);
|
||||||
return {
|
return {
|
||||||
id: draft
|
id: draft
|
||||||
? `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
? `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
||||||
@@ -459,10 +567,12 @@ function toEditable(
|
|||||||
pipelineId: config.pipeline_id,
|
pipelineId: config.pipeline_id,
|
||||||
promptSetId: config.prompt_set_id,
|
promptSetId: config.prompt_set_id,
|
||||||
toolKeys: config.tool_keys || [],
|
toolKeys: config.tool_keys || [],
|
||||||
|
toolKeysInput: (config.tool_keys || []).join(", "),
|
||||||
prompts: config.prompt_dict || {},
|
prompts: config.prompt_dict || {},
|
||||||
apiKey: config.api_key || DEFAULT_API_KEY,
|
apiKey: config.api_key || DEFAULT_API_KEY,
|
||||||
llmName: DEFAULT_LLM_NAME,
|
llmName: DEFAULT_LLM_NAME,
|
||||||
actBackend: DEFAULT_DEEPAGENT_ACT_BACKEND,
|
actBackend: deepAgentState.actBackend,
|
||||||
|
fileBackendConfig: deepAgentState.fileBackendConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,10 +860,15 @@ export default function App() {
|
|||||||
graphId,
|
graphId,
|
||||||
prompts: { ...defaults.prompt_dict },
|
prompts: { ...defaults.prompt_dict },
|
||||||
toolKeys: defaults.tool_keys || [],
|
toolKeys: defaults.tool_keys || [],
|
||||||
|
toolKeysInput: (defaults.tool_keys || []).join(", "),
|
||||||
actBackend:
|
actBackend:
|
||||||
graphId === "deepagent"
|
graphId === "deepagent"
|
||||||
? prev.actBackend || DEFAULT_DEEPAGENT_ACT_BACKEND
|
? prev.actBackend || DEFAULT_DEEPAGENT_ACT_BACKEND
|
||||||
: DEFAULT_DEEPAGENT_ACT_BACKEND,
|
: DEFAULT_DEEPAGENT_ACT_BACKEND,
|
||||||
|
fileBackendConfig:
|
||||||
|
graphId === "deepagent"
|
||||||
|
? prev.fileBackendConfig || getDefaultFileBackendConfig(DEFAULT_DEEPAGENT_ACT_BACKEND)
|
||||||
|
: getDefaultFileBackendConfig(DEFAULT_DEEPAGENT_ACT_BACKEND),
|
||||||
};
|
};
|
||||||
if (next.isDraft) {
|
if (next.isDraft) {
|
||||||
setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft)));
|
setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft)));
|
||||||
@@ -787,6 +902,24 @@ export default function App() {
|
|||||||
setEditorAndSyncDraft((prev) => ({ ...prev, [key]: value }));
|
setEditorAndSyncDraft((prev) => ({ ...prev, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateActBackend(newBackend: DeepAgentActBackend): void {
|
||||||
|
setEditorAndSyncDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
actBackend: newBackend,
|
||||||
|
fileBackendConfig: getDefaultFileBackendConfig(newBackend),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileBackendConfig(key: keyof FileBackendConfig, value: string): void {
|
||||||
|
setEditorAndSyncDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
fileBackendConfig: {
|
||||||
|
...prev.fileBackendConfig,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function updatePrompt(key: string, value: string): void {
|
function updatePrompt(key: string, value: string): void {
|
||||||
setEditorAndSyncDraft((prev) => ({
|
setEditorAndSyncDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -1116,6 +1249,7 @@ export default function App() {
|
|||||||
|
|
||||||
async function sendAgentChatMessage(): Promise<void> {
|
async function sendAgentChatMessage(): Promise<void> {
|
||||||
const pipelineId = (chatPipelineId || "").trim();
|
const pipelineId = (chatPipelineId || "").trim();
|
||||||
|
const conversationId = chatConversationId;
|
||||||
const message = chatInput.trim();
|
const message = chatInput.trim();
|
||||||
if (!pipelineId || !message || chatSending) {
|
if (!pipelineId || !message || chatSending) {
|
||||||
return;
|
return;
|
||||||
@@ -1145,7 +1279,7 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
await streamAgentChatResponse({
|
await streamAgentChatResponse({
|
||||||
appId: pipelineId,
|
appId: pipelineId,
|
||||||
sessionId: chatConversationId,
|
sessionId: conversationId,
|
||||||
apiKey: authKey,
|
apiKey: authKey,
|
||||||
message,
|
message,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -1162,6 +1296,34 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Some runtimes namespace thread ids as "<pipeline_id>:<session_id>" when persisting.
|
||||||
|
// Try both IDs and fail soft so a successful streamed reply never turns into a UI error.
|
||||||
|
const candidateConversationIds = [
|
||||||
|
conversationId,
|
||||||
|
`${pipelineId}:${conversationId}`,
|
||||||
|
];
|
||||||
|
let reloaded = false;
|
||||||
|
for (const candidateId of candidateConversationIds) {
|
||||||
|
try {
|
||||||
|
const storedMessages = await getPipelineConversationMessages(
|
||||||
|
pipelineId,
|
||||||
|
candidateId
|
||||||
|
);
|
||||||
|
const normalizedMessages = storedMessages
|
||||||
|
.map(mapConversationMessageToAgentChatMessage)
|
||||||
|
.filter((item): item is AgentChatMessage => item !== null);
|
||||||
|
if (normalizedMessages.length > 0) {
|
||||||
|
setChatMessages(normalizedMessages);
|
||||||
|
reloaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore refresh failures; keep streamed content visible.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!reloaded) {
|
||||||
|
// Keep existing streamed messages without surfacing a false error state.
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error).message === "Request cancelled") {
|
if ((error as Error).message === "Request cancelled") {
|
||||||
setChatMessages((prev) =>
|
setChatMessages((prev) =>
|
||||||
@@ -1366,8 +1528,12 @@ export default function App() {
|
|||||||
<label>
|
<label>
|
||||||
tool_keys (comma separated)
|
tool_keys (comma separated)
|
||||||
<input
|
<input
|
||||||
value={editor.toolKeys.join(", ")}
|
value={editor.toolKeysInput}
|
||||||
onChange={(e) => updateEditor("toolKeys", parseToolCsv(e.target.value))}
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
updateEditor("toolKeysInput", raw);
|
||||||
|
updateEditor("toolKeys", parseToolCsv(raw));
|
||||||
|
}}
|
||||||
placeholder="tool_a, tool_b"
|
placeholder="tool_a, tool_b"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
@@ -1397,25 +1563,69 @@ export default function App() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{editor.graphId === "deepagent" ? (
|
{editor.graphId === "deepagent" ? (
|
||||||
<label>
|
<>
|
||||||
act_bkend
|
<label>
|
||||||
<select
|
act_bkend
|
||||||
value={editor.actBackend}
|
<select
|
||||||
onChange={(e) =>
|
value={editor.actBackend}
|
||||||
updateEditor(
|
onChange={(e) =>
|
||||||
"actBackend",
|
updateActBackend(normalizeDeepAgentActBackend(e.target.value))
|
||||||
normalizeDeepAgentActBackend(e.target.value)
|
}
|
||||||
)
|
disabled={busy}
|
||||||
}
|
>
|
||||||
disabled={busy}
|
{DEEPAGENT_BACKEND_OPTIONS.map((option) => (
|
||||||
>
|
<option key={option.value} value={option.value}>
|
||||||
{DEEPAGENT_BACKEND_OPTIONS.map((option) => (
|
{option.label}
|
||||||
<option key={option.value} value={option.value}>
|
</option>
|
||||||
{option.label}
|
))}
|
||||||
</option>
|
</select>
|
||||||
))}
|
</label>
|
||||||
</select>
|
|
||||||
</label>
|
<div className="file-backend-config">
|
||||||
|
<h3>File Backend Config</h3>
|
||||||
|
{editor.actBackend === "daytona_sandbox" ? (
|
||||||
|
<label>
|
||||||
|
api_key
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={editor.fileBackendConfig.api_key || ""}
|
||||||
|
onChange={(e) => updateFileBackendConfig("api_key", e.target.value)}
|
||||||
|
placeholder="Daytona API key"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
{editor.actBackend === "local_shell" ? (
|
||||||
|
<label>
|
||||||
|
workspace_dir
|
||||||
|
<input
|
||||||
|
value={editor.fileBackendConfig.workspace_dir || ""}
|
||||||
|
onChange={(e) => updateFileBackendConfig("workspace_dir", e.target.value)}
|
||||||
|
placeholder="./workspace"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
<label>
|
||||||
|
skills_dir
|
||||||
|
<input
|
||||||
|
value={editor.fileBackendConfig.skills_dir || ""}
|
||||||
|
onChange={(e) => updateFileBackendConfig("skills_dir", e.target.value)}
|
||||||
|
placeholder="./assets/skills"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
rt_skills_dir
|
||||||
|
<input
|
||||||
|
value={editor.fileBackendConfig.rt_skills_dir || ""}
|
||||||
|
onChange={(e) => updateFileBackendConfig("rt_skills_dir", e.target.value)}
|
||||||
|
placeholder="/skills"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="prompt-section">
|
<div className="prompt-section">
|
||||||
@@ -1748,9 +1958,15 @@ export default function App() {
|
|||||||
chatMessages.map((message) => (
|
chatMessages.map((message) => (
|
||||||
<article
|
<article
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`chat-modal-message ${message.role === "assistant" ? "assistant" : "user"}`}
|
className={`chat-modal-message ${message.role}`}
|
||||||
>
|
>
|
||||||
<strong>{message.role === "assistant" ? "Agent" : "You"}</strong>
|
<strong>
|
||||||
|
{message.role === "assistant"
|
||||||
|
? "Agent"
|
||||||
|
: message.role === "tool"
|
||||||
|
? "Tool"
|
||||||
|
: "You"}
|
||||||
|
</strong>
|
||||||
<div className="chat-message-content">
|
<div className="chat-message-content">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
{message.content || (chatSending && message.role === "assistant" ? "..." : "")}
|
{message.content || (chatSending && message.role === "assistant" ? "..." : "")}
|
||||||
|
|||||||
22
frontend/src/api/frontApis.test.ts
Normal file
22
frontend/src/api/frontApis.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { joinApiUrl } from "./frontApis";
|
||||||
|
|
||||||
|
describe("joinApiUrl", () => {
|
||||||
|
it("keeps same-origin paths when base url is slash", () => {
|
||||||
|
expect(joinApiUrl("/", "/v1/pipelines")).toBe("/v1/pipelines");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("joins absolute host and trims trailing slash", () => {
|
||||||
|
expect(joinApiUrl("http://127.0.0.1:8500/", "/v1/pipelines")).toBe(
|
||||||
|
"http://127.0.0.1:8500/v1/pipelines"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts path without leading slash", () => {
|
||||||
|
expect(joinApiUrl("http://127.0.0.1:8500", "v1/pipelines")).toBe(
|
||||||
|
"http://127.0.0.1:8500/v1/pipelines"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -19,8 +19,19 @@ import type {
|
|||||||
RuntimeAuthInfoResponse,
|
RuntimeAuthInfoResponse,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL = import.meta.env.VITE_FRONT_API_BASE_URL?.trim() || "/";
|
||||||
import.meta.env.VITE_FRONT_API_BASE_URL?.trim() || "http://127.0.0.1:8500";
|
|
||||||
|
export function joinApiUrl(baseUrl: string, path: string): string {
|
||||||
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
const normalizedBase = baseUrl.trim();
|
||||||
|
|
||||||
|
// "/" is commonly used in Docker+nginx builds and should resolve as same-origin.
|
||||||
|
if (!normalizedBase || normalizedBase === "/") {
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalizedBase.replace(/\/+$/, "")}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Log which backend the frontend is targeting on startup, with file + line hint.
|
// Log which backend the frontend is targeting on startup, with file + line hint.
|
||||||
// This runs once when the module is loaded.
|
// This runs once when the module is loaded.
|
||||||
@@ -30,7 +41,8 @@ console.info(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
const url = joinApiUrl(API_BASE_URL, path);
|
||||||
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(init?.headers || {}),
|
...(init?.headers || {}),
|
||||||
@@ -49,7 +61,24 @@ async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
}
|
}
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
return (await response.json()) as T;
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyText = await response.text();
|
||||||
|
if (!bodyText.trim()) {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(bodyText) as T;
|
||||||
|
} catch {
|
||||||
|
const preview = bodyText.slice(0, 160).replace(/\s+/g, " ").trim();
|
||||||
|
throw new Error(
|
||||||
|
`Expected JSON response from ${url}, but received non-JSON content: ${preview || "<empty>"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listAvailableGraphs(): Promise<AvailableGraphsResponse> {
|
export function listAvailableGraphs(): Promise<AvailableGraphsResponse> {
|
||||||
@@ -189,7 +218,10 @@ export async function streamAgentChatResponse(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { appId, sessionId, apiKey, message, onText, signal } = options;
|
const { appId, sessionId, apiKey, message, onText, signal } = options;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE_URL}/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses`,
|
joinApiUrl(
|
||||||
|
API_BASE_URL,
|
||||||
|
`/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses`
|
||||||
|
),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-section,
|
.prompt-section,
|
||||||
|
.file-backend-config,
|
||||||
.run-info {
|
.run-info {
|
||||||
border: 1px solid #dbe2ea;
|
border: 1px solid #dbe2ea;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -232,6 +233,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prompt-section h3,
|
.prompt-section h3,
|
||||||
|
.file-backend-config h3,
|
||||||
.run-info h3 {
|
.run-info h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
@@ -609,6 +611,10 @@ button:disabled {
|
|||||||
border-left: 3px solid #26a269;
|
border-left: 3px solid #26a269;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-modal-message.tool {
|
||||||
|
border-left: 3px solid #8e6bd8;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-modal-message p {
|
.chat-modal-message p {
|
||||||
margin: 6px 0 0 0;
|
margin: 6px 0 0 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type GraphConfigReadResponse = {
|
|||||||
tool_keys: string[];
|
tool_keys: string[];
|
||||||
prompt_dict: Record<string, string>;
|
prompt_dict: Record<string, string>;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
|
graph_params?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphConfigUpsertRequest = {
|
export type GraphConfigUpsertRequest = {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/activeConfigSelection.test.ts","./src/activeConfigSelection.ts","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/api/frontApis.ts"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/activeConfigSelection.test.ts","./src/activeConfigSelection.ts","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/api/frontApis.test.ts","./src/api/frontApis.ts"],"version":"5.9.3"}
|
||||||
@@ -4,5 +4,15 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/v1": {
|
||||||
|
target: "http://127.0.0.1:8500",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
"/apps": {
|
||||||
|
target: "http://127.0.0.1:8500",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/v1": {
|
||||||
|
target: "http://127.0.0.1:8500",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
"/apps": {
|
||||||
|
target: "http://127.0.0.1:8500",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from lang_agent.config.core_config import (
|
|||||||
LLMKeyConfig,
|
LLMKeyConfig,
|
||||||
LLMNodeConfig,
|
LLMNodeConfig,
|
||||||
load_tyro_conf,
|
load_tyro_conf,
|
||||||
|
resolve_llm_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lang_agent.config.constants import (
|
from lang_agent.config.constants import (
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_llm_api_key(api_key: Optional[str]) -> Optional[str]:
|
||||||
|
"""Resolve the API key for OpenAI-compatible providers."""
|
||||||
|
if api_key not in (None, "", "wrong-key"):
|
||||||
|
resolved_key = api_key
|
||||||
|
else:
|
||||||
|
resolved_key = os.environ.get("ALI_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||||
|
|
||||||
|
# Some OpenAI-compatible integrations still read OPENAI_API_KEY from env.
|
||||||
|
if resolved_key and not os.environ.get("OPENAI_API_KEY"):
|
||||||
|
os.environ["OPENAI_API_KEY"] = resolved_key
|
||||||
|
|
||||||
|
return resolved_key
|
||||||
|
|
||||||
## NOTE: base classes taken from nerfstudio
|
## NOTE: base classes taken from nerfstudio
|
||||||
class PrintableConfig:
|
class PrintableConfig:
|
||||||
"""
|
"""
|
||||||
@@ -99,12 +113,12 @@ class LLMKeyConfig(InstantiateConfig):
|
|||||||
"""api key for llm"""
|
"""api key for llm"""
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.api_key == "wrong-key" or self.api_key is None:
|
original_api_key = self.api_key
|
||||||
self.api_key = os.environ.get("ALI_API_KEY")
|
self.api_key = resolve_llm_api_key(self.api_key)
|
||||||
if self.api_key is None:
|
if self.api_key is None:
|
||||||
logger.error(f"no ALI_API_KEY provided for embedding")
|
logger.error("no ALI_API_KEY or OPENAI_API_KEY provided for embedding")
|
||||||
else:
|
elif original_api_key in (None, "", "wrong-key"):
|
||||||
logger.info("ALI_API_KEY loaded from environ")
|
logger.info("LLM API key loaded from environment")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lang_agent.config.constants import (
|
|||||||
MCP_CONFIG_DEFAULT_CONTENT,
|
MCP_CONFIG_DEFAULT_CONTENT,
|
||||||
PIPELINE_REGISTRY_PATH,
|
PIPELINE_REGISTRY_PATH,
|
||||||
)
|
)
|
||||||
|
from lang_agent.config.core_config import load_tyro_conf
|
||||||
from lang_agent.front_api.build_server_utils import (
|
from lang_agent.front_api.build_server_utils import (
|
||||||
GRAPH_BUILD_FNCS,
|
GRAPH_BUILD_FNCS,
|
||||||
update_pipeline_registry,
|
update_pipeline_registry,
|
||||||
@@ -55,6 +56,7 @@ class GraphConfigReadResponse(BaseModel):
|
|||||||
tool_keys: List[str]
|
tool_keys: List[str]
|
||||||
prompt_dict: Dict[str, str]
|
prompt_dict: Dict[str, str]
|
||||||
api_key: str = Field(default="")
|
api_key: str = Field(default="")
|
||||||
|
graph_params: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class GraphConfigListItem(BaseModel):
|
class GraphConfigListItem(BaseModel):
|
||||||
@@ -325,6 +327,81 @@ def _normalize_pipeline_spec(pipeline_id: str, spec: Dict[str, Any]) -> Pipeline
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_config_path(config_file: str) -> str:
|
||||||
|
if osp.isabs(config_file):
|
||||||
|
return config_file
|
||||||
|
return osp.join(_PROJECT_ROOT, config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_deepagent_backend_name(file_backend_config: Any) -> Optional[str]:
|
||||||
|
if file_backend_config is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
type_names = {
|
||||||
|
type(file_backend_config).__name__.lower(),
|
||||||
|
getattr(getattr(file_backend_config, "_target", None), "__name__", "").lower(),
|
||||||
|
}
|
||||||
|
if any("statebk" in name for name in type_names):
|
||||||
|
return "state_bk"
|
||||||
|
if any("localshell" in name for name in type_names):
|
||||||
|
return "local_shell"
|
||||||
|
if any("daytona" in name for name in type_names):
|
||||||
|
return "daytona_sandbox"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_graph_params_from_config(graph_id: Optional[str], loaded_cfg: Any) -> Dict[str, Any]:
|
||||||
|
if graph_id != "deepagent":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
graph_config = getattr(loaded_cfg, "graph_config", None)
|
||||||
|
file_backend_config = getattr(graph_config, "file_backend_config", None)
|
||||||
|
if file_backend_config is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
graph_params: Dict[str, Any] = {}
|
||||||
|
act_bkend = _normalize_deepagent_backend_name(file_backend_config)
|
||||||
|
if act_bkend:
|
||||||
|
graph_params["act_bkend"] = act_bkend
|
||||||
|
|
||||||
|
serialized_backend_config: Dict[str, Any] = {}
|
||||||
|
for key in ("skills_dir", "rt_skills_dir", "workspace_dir", "api_key"):
|
||||||
|
value = getattr(file_backend_config, key, None)
|
||||||
|
if value is not None:
|
||||||
|
serialized_backend_config[key] = value
|
||||||
|
|
||||||
|
if serialized_backend_config:
|
||||||
|
graph_params["file_backend_config"] = serialized_backend_config
|
||||||
|
|
||||||
|
return graph_params
|
||||||
|
|
||||||
|
|
||||||
|
def _load_graph_params_for_pipeline(
|
||||||
|
pipeline_id: str, graph_id: Optional[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
registry = _read_pipeline_registry()
|
||||||
|
pipeline_spec = registry.get("pipelines", {}).get(pipeline_id, {})
|
||||||
|
config_file = ""
|
||||||
|
if isinstance(pipeline_spec, dict):
|
||||||
|
config_file = str(pipeline_spec.get("config_file") or "").strip()
|
||||||
|
if not config_file:
|
||||||
|
fallback = osp.join(_PROJECT_ROOT, "configs", "pipelines", f"{pipeline_id}.yaml")
|
||||||
|
if osp.exists(fallback):
|
||||||
|
config_file = fallback
|
||||||
|
if not config_file:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
config_path = _resolve_config_path(config_file)
|
||||||
|
if not osp.exists(config_path):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
loaded_cfg = load_tyro_conf(config_path)
|
||||||
|
return _extract_graph_params_from_config(graph_id, loaded_cfg)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_api_key_policy(api_key: str, policy: Dict[str, Any]) -> ApiKeyPolicyItem:
|
def _normalize_api_key_policy(api_key: str, policy: Dict[str, Any]) -> ApiKeyPolicyItem:
|
||||||
if not isinstance(policy, dict):
|
if not isinstance(policy, dict):
|
||||||
raise ValueError(f"api key policy for '{api_key}' must be an object")
|
raise ValueError(f"api key policy for '{api_key}' must be an object")
|
||||||
@@ -428,6 +505,9 @@ async def get_default_graph_config(pipeline_id: str):
|
|||||||
tool_keys=tool_keys,
|
tool_keys=tool_keys,
|
||||||
prompt_dict=prompt_dict,
|
prompt_dict=prompt_dict,
|
||||||
api_key=(active.get("api_key") or ""),
|
api_key=(active.get("api_key") or ""),
|
||||||
|
graph_params=_load_graph_params_for_pipeline(
|
||||||
|
pipeline_id, active.get("graph_id")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -466,6 +546,9 @@ async def get_graph_config(pipeline_id: str, prompt_set_id: str):
|
|||||||
tool_keys=tool_keys,
|
tool_keys=tool_keys,
|
||||||
prompt_dict=prompt_dict,
|
prompt_dict=prompt_dict,
|
||||||
api_key=(meta.get("api_key") or ""),
|
api_key=(meta.get("api_key") or ""),
|
||||||
|
graph_params=_load_graph_params_for_pipeline(
|
||||||
|
pipeline_id, meta.get("graph_id")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Dict, List, Literal
|
from typing import Any, Dict, List, Literal, Optional
|
||||||
import os
|
import os
|
||||||
import os.path as osp
|
import os.path as osp
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -75,23 +75,42 @@ def build_route(
|
|||||||
**_: Any,
|
**_: Any,
|
||||||
):
|
):
|
||||||
cmd_opt = [
|
cmd_opt = [
|
||||||
"--pipeline.pipeline-id", pipeline_id,
|
"--pipeline.pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--pipeline.llm-name",
|
||||||
|
llm_name,
|
||||||
"route", # ------------
|
"route", # ------------
|
||||||
"--llm-name", llm_name,
|
"--llm-name",
|
||||||
"--api-key", api_key,
|
llm_name,
|
||||||
"--pipeline-id", pipeline_id,
|
"--api-key",
|
||||||
"--prompt-set-id", prompt_set,
|
api_key,
|
||||||
"tool_node", # ------------
|
"--pipeline-id",
|
||||||
"--llm-name", llm_name,
|
pipeline_id,
|
||||||
"--api-key", api_key,
|
"--prompt-set-id",
|
||||||
"--pipeline-id", pipeline_id,
|
prompt_set,
|
||||||
"--prompt-set-id", prompt_set,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if tool_keys:
|
if tool_keys:
|
||||||
cmd_opt.extend(
|
cmd_opt.extend(
|
||||||
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
||||||
)
|
)
|
||||||
|
# Tyro parses list options greedily across positional subcommands; repeat a
|
||||||
|
# parent-level option to terminate list parsing before `tool_node`.
|
||||||
|
cmd_opt.extend(["--pipeline-id", pipeline_id])
|
||||||
|
|
||||||
|
cmd_opt.extend(
|
||||||
|
[
|
||||||
|
"tool_node", # ------------
|
||||||
|
"--llm-name",
|
||||||
|
llm_name,
|
||||||
|
"--api-key",
|
||||||
|
api_key,
|
||||||
|
"--pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--prompt-set-id",
|
||||||
|
prompt_set,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return _build_and_load_pipeline_config(pipeline_id, pipeline_config_dir, cmd_opt)
|
return _build_and_load_pipeline_config(pipeline_id, pipeline_config_dir, cmd_opt)
|
||||||
|
|
||||||
@@ -106,13 +125,20 @@ def build_react(
|
|||||||
**_: Any,
|
**_: Any,
|
||||||
):
|
):
|
||||||
cmd_opt = [
|
cmd_opt = [
|
||||||
"--pipeline.pipeline-id", pipeline_id,
|
"--pipeline.pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--pipeline.llm-name",
|
||||||
|
llm_name,
|
||||||
"react", # ------------
|
"react", # ------------
|
||||||
"--llm-name", llm_name,
|
"--llm-name",
|
||||||
"--api-key", api_key,
|
llm_name,
|
||||||
"--pipeline-id", pipeline_id,
|
"--api-key",
|
||||||
"--prompt-set-id", prompt_set,
|
api_key,
|
||||||
]
|
"--pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--prompt-set-id",
|
||||||
|
prompt_set,
|
||||||
|
]
|
||||||
if tool_keys:
|
if tool_keys:
|
||||||
cmd_opt.extend(
|
cmd_opt.extend(
|
||||||
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
||||||
@@ -136,6 +162,7 @@ def build_deep_agent(
|
|||||||
"daytona_sandbox",
|
"daytona_sandbox",
|
||||||
"daytonasandbox",
|
"daytonasandbox",
|
||||||
] = "state_bk",
|
] = "state_bk",
|
||||||
|
file_backend_config: Optional[Dict[str, Any]] = None,
|
||||||
**_: Any,
|
**_: Any,
|
||||||
):
|
):
|
||||||
backend_subcommand = _DEEP_AGENT_BACKEND_ALIASES.get(act_bkend)
|
backend_subcommand = _DEEP_AGENT_BACKEND_ALIASES.get(act_bkend)
|
||||||
@@ -146,22 +173,48 @@ def build_deep_agent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cmd_opt = [
|
cmd_opt = [
|
||||||
"--pipeline.pipeline-id", pipeline_id,
|
"--pipeline.pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--pipeline.llm-name",
|
||||||
|
llm_name,
|
||||||
"deepagent",
|
"deepagent",
|
||||||
"--llm-name", llm_name,
|
"--llm-name",
|
||||||
"--api-key", api_key,
|
llm_name,
|
||||||
"--pipeline-id", pipeline_id,
|
"--api-key",
|
||||||
"--prompt-set-id", prompt_set,
|
api_key,
|
||||||
backend_subcommand,
|
"--pipeline-id",
|
||||||
|
pipeline_id,
|
||||||
|
"--prompt-set-id",
|
||||||
|
prompt_set,
|
||||||
]
|
]
|
||||||
|
|
||||||
if tool_keys:
|
if tool_keys:
|
||||||
cmd_opt.extend(
|
cmd_opt.extend(
|
||||||
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
["--tool-manager-config.client-tool-manager.tool-keys", *tool_keys]
|
||||||
)
|
)
|
||||||
|
cmd_opt.extend(["--pipeline-id", pipeline_id])
|
||||||
|
|
||||||
|
cmd_opt.append(backend_subcommand)
|
||||||
|
|
||||||
|
if file_backend_config:
|
||||||
|
if "skills_dir" in file_backend_config and file_backend_config["skills_dir"]:
|
||||||
|
cmd_opt.extend(["--skills-dir", file_backend_config["skills_dir"]])
|
||||||
|
if (
|
||||||
|
"rt_skills_dir" in file_backend_config
|
||||||
|
and file_backend_config["rt_skills_dir"]
|
||||||
|
):
|
||||||
|
cmd_opt.extend(["--rt-skills-dir", file_backend_config["rt_skills_dir"]])
|
||||||
|
if (
|
||||||
|
"workspace_dir" in file_backend_config
|
||||||
|
and file_backend_config["workspace_dir"]
|
||||||
|
):
|
||||||
|
cmd_opt.extend(["--workspace-dir", file_backend_config["workspace_dir"]])
|
||||||
|
if "api_key" in file_backend_config and file_backend_config["api_key"]:
|
||||||
|
cmd_opt.extend(["--api-key", file_backend_config["api_key"]])
|
||||||
|
|
||||||
return _build_and_load_pipeline_config(pipeline_id, pipeline_config_dir, cmd_opt)
|
return _build_and_load_pipeline_config(pipeline_id, pipeline_config_dir, cmd_opt)
|
||||||
|
|
||||||
|
|
||||||
# {pipeline_id: build_function}
|
# {pipeline_id: build_function}
|
||||||
GRAPH_BUILD_FNCS = {
|
GRAPH_BUILD_FNCS = {
|
||||||
"routing": build_route,
|
"routing": build_route,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from dataclasses import dataclass, field, is_dataclass
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import tyro
|
|
||||||
import os.path as osp
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lang_agent.config import InstantiateConfig
|
||||||
|
|
||||||
|
|
||||||
class BaseFilesystemBackend(ABC):
|
class BaseFilesystemBackend(ABC):
|
||||||
@@ -25,4 +27,25 @@ class BaseFilesystemBackend(ABC):
|
|||||||
if hasattr(self.config, "rt_skills_dir"):
|
if hasattr(self.config, "rt_skills_dir"):
|
||||||
return {"skills" : [self.config.rt_skills_dir]}
|
return {"skills" : [self.config.rt_skills_dir]}
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FilesystemBackendConfig(InstantiateConfig):
|
||||||
|
"""
|
||||||
|
Shared filesystem backend config behavior.
|
||||||
|
If subclasses define these fields, this hook ensures they exist:
|
||||||
|
- skills_dir
|
||||||
|
- workspace_dir
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _ensure_dir_if_present(self, attr_name: str) -> None:
|
||||||
|
path = getattr(self, attr_name, None)
|
||||||
|
if not isinstance(path, str) or not path.strip():
|
||||||
|
return
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
logger.info(f"Ensured {attr_name} exists: {path}")
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._ensure_dir_if_present("skills_dir")
|
||||||
|
self._ensure_dir_if_present("workspace_dir")
|
||||||
@@ -8,13 +8,12 @@ from loguru import logger
|
|||||||
from daytona import Daytona, DaytonaConfig, FileUpload
|
from daytona import Daytona, DaytonaConfig, FileUpload
|
||||||
from langchain_daytona import DaytonaSandbox
|
from langchain_daytona import DaytonaSandbox
|
||||||
|
|
||||||
from lang_agent.config import InstantiateConfig
|
from lang_agent.fs_bkends.base import BaseFilesystemBackend, FilesystemBackendConfig
|
||||||
from lang_agent.fs_bkends import BaseFilesystemBackend
|
|
||||||
|
|
||||||
|
|
||||||
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
||||||
@dataclass
|
@dataclass
|
||||||
class DaytonaSandboxConfig(InstantiateConfig):
|
class DaytonaSandboxConfig(FilesystemBackendConfig):
|
||||||
_target: Type = field(default_factory=lambda: DaytonaSandboxBk)
|
_target: Type = field(default_factory=lambda: DaytonaSandboxBk)
|
||||||
|
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
@@ -27,6 +26,7 @@ class DaytonaSandboxConfig(InstantiateConfig):
|
|||||||
"""runtime skills path inside the sandbox (auto-set from sandbox workdir)"""
|
"""runtime skills path inside the sandbox (auto-set from sandbox workdir)"""
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
if self.api_key is None:
|
if self.api_key is None:
|
||||||
self.api_key = os.environ.get("DAYTONA_API_KEY")
|
self.api_key = os.environ.get("DAYTONA_API_KEY")
|
||||||
if self.api_key is None:
|
if self.api_key is None:
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
from dataclasses import dataclass, field, is_dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Type, TypedDict, Literal, Dict, List, Tuple, Optional
|
from typing import Type
|
||||||
import tyro
|
import tyro
|
||||||
import os.path as osp
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import glob
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from deepagents.backends.utils import create_file_data
|
|
||||||
from deepagents.backends import LocalShellBackend
|
from deepagents.backends import LocalShellBackend
|
||||||
|
|
||||||
from lang_agent.config import InstantiateConfig
|
from lang_agent.fs_bkends.base import BaseFilesystemBackend, FilesystemBackendConfig
|
||||||
from lang_agent.fs_bkends import BaseFilesystemBackend
|
|
||||||
|
|
||||||
|
|
||||||
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalShellConfig(InstantiateConfig):
|
class LocalShellConfig(FilesystemBackendConfig):
|
||||||
_target:Type = field(default_factory=lambda:LocalShell)
|
_target:Type = field(default_factory=lambda:LocalShell)
|
||||||
|
|
||||||
workspace_dir:str = "./workspace"
|
workspace_dir:str = "./workspace"
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
from dataclasses import dataclass, field, is_dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Type, TypedDict, Literal, Dict, List, Tuple, Optional
|
from typing import Type
|
||||||
import tyro
|
import tyro
|
||||||
import os.path as osp
|
import os.path as osp
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import glob
|
import glob
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from deepagents.backends.utils import create_file_data
|
from deepagents.backends.utils import create_file_data
|
||||||
from deepagents.backends import StateBackend
|
from deepagents.backends import StateBackend
|
||||||
|
|
||||||
from lang_agent.config import InstantiateConfig
|
from lang_agent.fs_bkends.base import BaseFilesystemBackend, FilesystemBackendConfig
|
||||||
from lang_agent.fs_bkends import BaseFilesystemBackend
|
|
||||||
|
|
||||||
def read_as_utf8(file_path:str):
|
def read_as_utf8(file_path:str):
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
@@ -31,7 +29,7 @@ def build_skill_fs_dict(skill_dir:str, virt_path:str="/skills"):
|
|||||||
|
|
||||||
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
||||||
@dataclass
|
@dataclass
|
||||||
class StateBkConfig(InstantiateConfig):
|
class StateBkConfig(FilesystemBackendConfig):
|
||||||
_target:Type = field(default_factory=lambda:StateBk)
|
_target:Type = field(default_factory=lambda:StateBk)
|
||||||
|
|
||||||
skills_dir:str = "./assets/skills"
|
skills_dir:str = "./assets/skills"
|
||||||
@@ -40,10 +38,6 @@ class StateBkConfig(InstantiateConfig):
|
|||||||
rt_skills_dir:str = "/skills"
|
rt_skills_dir:str = "/skills"
|
||||||
"""path to directory with skills in runtime directory"""
|
"""path to directory with skills in runtime directory"""
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
err_msg = f"{self.skills_dir} does not exist"
|
|
||||||
assert osp.exists(self.skills_dir), err_msg
|
|
||||||
|
|
||||||
|
|
||||||
class StateBk(BaseFilesystemBackend):
|
class StateBk(BaseFilesystemBackend):
|
||||||
def __init__(self, config:StateBkConfig):
|
def __init__(self, config:StateBkConfig):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage
|
|||||||
from langchain.agents import create_agent
|
from langchain.agents import create_agent
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
|
||||||
from lang_agent.config import LLMNodeConfig, load_tyro_conf
|
from lang_agent.config import LLMNodeConfig, load_tyro_conf, resolve_llm_api_key
|
||||||
from lang_agent.graphs import AnnotatedGraph, ReactGraphConfig, RoutingConfig
|
from lang_agent.graphs import AnnotatedGraph, ReactGraphConfig, RoutingConfig
|
||||||
from lang_agent.base import GraphBase
|
from lang_agent.base import GraphBase
|
||||||
from lang_agent.components import conv_store
|
from lang_agent.components import conv_store
|
||||||
@@ -104,7 +104,13 @@ class Pipeline:
|
|||||||
if self.config.base_url is not None
|
if self.config.base_url is not None
|
||||||
else self.config.graph_config.base_url
|
else self.config.graph_config.base_url
|
||||||
)
|
)
|
||||||
self.config.graph_config.api_key = self.config.api_key
|
pipeline_api_key = resolve_llm_api_key(self.config.api_key)
|
||||||
|
graph_api_key = resolve_llm_api_key(
|
||||||
|
getattr(self.config.graph_config, "api_key", None)
|
||||||
|
)
|
||||||
|
resolved_api_key = pipeline_api_key or graph_api_key
|
||||||
|
self.config.api_key = resolved_api_key
|
||||||
|
self.config.graph_config.api_key = resolved_api_key
|
||||||
|
|
||||||
self.graph: GraphBase = self.config.graph_config.setup()
|
self.graph: GraphBase = self.config.graph_config.setup()
|
||||||
|
|
||||||
|
|||||||
85
nginx.conf
Normal file
85
nginx.conf
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# Upstream backend
|
||||||
|
upstream backend {
|
||||||
|
server backend:8500;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Always revalidate the SPA entrypoint so clients pick up the latest
|
||||||
|
# hashed JS bundle after redeploys.
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
|
||||||
|
add_header Pragma "no-cache" always;
|
||||||
|
add_header Expires "0" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve frontend static files
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API requests to backend
|
||||||
|
location /v1/ {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy DashScope API requests
|
||||||
|
location /apps/ {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy v1/apps requests
|
||||||
|
location /v1/apps/ {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://backend/health;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +24,10 @@ dependencies = [
|
|||||||
"commentjson",
|
"commentjson",
|
||||||
"pandas",
|
"pandas",
|
||||||
"asgiref",
|
"asgiref",
|
||||||
"psycopg[binary]"
|
"psycopg[binary]",
|
||||||
|
"deepagents",
|
||||||
|
"daytona",
|
||||||
|
"langchain_daytona"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
40
scripts/init_database/00_init_user.sh
Executable file
40
scripts/init_database/00_init_user.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Initialize database user and database
|
||||||
|
# This script runs before SQL files in docker-entrypoint-initdb.d
|
||||||
|
# It must be named with 00_ prefix to run first
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_DB_NAME="${APP_DB_NAME:-ai_conversations}"
|
||||||
|
APP_DB_USER="${APP_DB_USER:-myapp_user}"
|
||||||
|
APP_DB_PASSWORD="${APP_DB_PASSWORD:-secure_password_123}"
|
||||||
|
|
||||||
|
echo "Creating database user: $APP_DB_USER"
|
||||||
|
# Create user
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_user WHERE usename = '$APP_DB_USER') THEN
|
||||||
|
CREATE USER $APP_DB_USER WITH PASSWORD '$APP_DB_PASSWORD';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
ALTER USER $APP_DB_USER CREATEDB;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
echo "Creating database: $APP_DB_NAME"
|
||||||
|
# Create database
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
SELECT 'CREATE DATABASE $APP_DB_NAME'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$APP_DB_NAME')\gexec
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE $APP_DB_NAME TO $APP_DB_USER;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
echo "Granting schema privileges"
|
||||||
|
# Grant schema privileges
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$APP_DB_NAME" <<-EOSQL
|
||||||
|
GRANT ALL ON SCHEMA public TO $APP_DB_USER;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
echo "Database initialization complete!"
|
||||||
|
|
||||||
25
scripts/init_database/01_run_sql_files.sh
Executable file
25
scripts/init_database/01_run_sql_files.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run SQL initialization files in the correct database context
|
||||||
|
# This script runs after 00_init_user.sh creates the database
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_DB_NAME="${APP_DB_NAME:-ai_conversations}"
|
||||||
|
|
||||||
|
echo "Running SQL initialization files in database: $APP_DB_NAME"
|
||||||
|
|
||||||
|
# Run create_conv_store.sql
|
||||||
|
if [ -f /docker-entrypoint-initdb.d/create_conv_store.sql ]; then
|
||||||
|
echo "Executing create_conv_store.sql..."
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$APP_DB_NAME" -f /docker-entrypoint-initdb.d/create_conv_store.sql
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run create_prompt_config.sql
|
||||||
|
if [ -f /docker-entrypoint-initdb.d/create_prompt_config.sql ]; then
|
||||||
|
echo "Executing create_prompt_config.sql..."
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$APP_DB_NAME" -f /docker-entrypoint-initdb.d/create_prompt_config.sql
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SQL initialization files completed!"
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
-- Create the messages table
|
-- Create the messages table
|
||||||
|
-- This script runs in the ai_conversations database context
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
conversation_id TEXT NOT NULL,
|
conversation_id TEXT NOT NULL,
|
||||||
@@ -13,4 +14,8 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages (conversation_id, sequence_number);
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages (conversation_id, sequence_number);
|
||||||
|
|
||||||
-- Index for fast lookup by pipeline_id
|
-- Index for fast lookup by pipeline_id
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_pipeline ON messages (pipeline_id);
|
CREATE INDEX IF NOT EXISTS idx_messages_pipeline ON messages (pipeline_id);
|
||||||
|
|
||||||
|
-- Grant permissions to app user
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO myapp_user;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO myapp_user;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
-- A prompt_set groups a full collection of prompts together.
|
-- A prompt_set groups a full collection of prompts together.
|
||||||
|
-- This script runs in the ai_conversations database context
|
||||||
-- Each pipeline can have many sets (versions, A/B variants, etc.);
|
-- Each pipeline can have many sets (versions, A/B variants, etc.);
|
||||||
-- exactly one should be marked is_active per pipeline.
|
-- exactly one should be marked is_active per pipeline.
|
||||||
CREATE TABLE IF NOT EXISTS prompt_sets (
|
CREATE TABLE IF NOT EXISTS prompt_sets (
|
||||||
@@ -41,6 +42,10 @@ CREATE TABLE IF NOT EXISTS prompt_templates (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_prompt_templates_set_id
|
CREATE INDEX IF NOT EXISTS idx_prompt_templates_set_id
|
||||||
ON prompt_templates(prompt_set_id);
|
ON prompt_templates(prompt_set_id);
|
||||||
|
|
||||||
|
-- Grant permissions to app user
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO myapp_user;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO myapp_user;
|
||||||
|
|
||||||
-- Seed: initial prompt set for lang_agent/graphs/routing.py
|
-- Seed: initial prompt set for lang_agent/graphs/routing.py
|
||||||
-- The pipeline_id can be used by RoutingConfig.pipeline_id to load these prompts.
|
-- The pipeline_id can be used by RoutingConfig.pipeline_id to load these prompts.
|
||||||
INSERT INTO prompt_sets (pipeline_id, graph_id, name, description, is_active, list)
|
INSERT INTO prompt_sets (pipeline_id, graph_id, name, description, is_active, list)
|
||||||
|
|||||||
49
scripts/init_database/init_all.sh
Executable file
49
scripts/init_database/init_all.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Database initialization script
|
||||||
|
# This script runs all SQL initialization files in the correct order
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DB_NAME="${POSTGRES_DB:-ai_conversations}"
|
||||||
|
DB_USER="${POSTGRES_USER:-myapp_user}"
|
||||||
|
DB_PASSWORD="${POSTGRES_PASSWORD:-secure_password_123}"
|
||||||
|
DB_HOST="${POSTGRES_HOST:-localhost}"
|
||||||
|
DB_PORT="${POSTGRES_PORT:-5432}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASSWORD"
|
||||||
|
|
||||||
|
echo "Initializing database: $DB_NAME on $DB_HOST:$DB_PORT"
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
until psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c '\q' 2>/dev/null; do
|
||||||
|
echo "Waiting for PostgreSQL to be ready..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "PostgreSQL is ready!"
|
||||||
|
|
||||||
|
# Create database if it doesn't exist
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres <<EOF
|
||||||
|
SELECT 'CREATE DATABASE $DB_NAME'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$DB_NAME')\gexec
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Grant privileges
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres <<EOF
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run initialization scripts in order
|
||||||
|
echo "Running database initialization scripts..."
|
||||||
|
|
||||||
|
# 1. Create conversation store tables
|
||||||
|
echo "Creating conversation store tables..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f /docker-entrypoint-initdb.d/create_conv_store.sql
|
||||||
|
|
||||||
|
# 2. Create prompt configuration tables
|
||||||
|
echo "Creating prompt configuration tables..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f /docker-entrypoint-initdb.d/create_prompt_config.sql
|
||||||
|
|
||||||
|
echo "Database initialization complete!"
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
conda init
|
conda init
|
||||||
conda activate lang
|
conda activate lang
|
||||||
37
scripts/shell_scripts/download_images.sh
Executable file
37
scripts/shell_scripts/download_images.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to download and package Docker images for offline use
|
||||||
|
# Run this on a machine with good Docker Hub access, then transfer images.tar to China
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Docker Image Downloader for Offline Use ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Images needed
|
||||||
|
IMAGES=(
|
||||||
|
"node:20-alpine"
|
||||||
|
"python:3.12-slim"
|
||||||
|
"postgres:16-alpine"
|
||||||
|
"nginx:alpine"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_FILE="images.tar"
|
||||||
|
|
||||||
|
echo "Pulling Docker images..."
|
||||||
|
for img in "${IMAGES[@]}"; do
|
||||||
|
echo " Pulling $img..."
|
||||||
|
docker pull "$img"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Saving to $OUTPUT_FILE..."
|
||||||
|
docker save "${IMAGES[@]}" -o "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done! File size:"
|
||||||
|
ls -lh "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To transfer to China machine and load:"
|
||||||
|
echo " scp images.tar user@china-machine:/path/"
|
||||||
|
echo " docker load < images.tar"
|
||||||
167
scripts/shell_scripts/install.sh
Executable file
167
scripts/shell_scripts/install.sh
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Installation script for LangChain Agent
|
||||||
|
# This script sets up and runs the entire application stack
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
ENV_FILE="$PROJECT_ROOT/.env"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== LangChain Agent Installation Script ===${NC}\n"
|
||||||
|
|
||||||
|
# Check for required tools
|
||||||
|
check_requirements() {
|
||||||
|
echo -e "${YELLOW}Checking requirements...${NC}"
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: Docker is not installed. Please install Docker first.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: docker-compose is not installed. Please install docker-compose first.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ All requirements met${NC}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create .env file if it doesn't exist
|
||||||
|
create_env_file() {
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo -e "${YELLOW}Creating .env file...${NC}"
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
# Database Configuration
|
||||||
|
POSTGRES_DB=ai_conversations
|
||||||
|
POSTGRES_USER=myapp_user
|
||||||
|
POSTGRES_PASSWORD=secure_password_123
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Backend Configuration
|
||||||
|
BACKEND_PORT=8500
|
||||||
|
|
||||||
|
# Frontend Configuration
|
||||||
|
FRONTEND_PORT=8080
|
||||||
|
|
||||||
|
# Database Connection String (used by backend)
|
||||||
|
CONN_STR=postgresql://myapp_user:secure_password_123@postgres:5432/ai_conversations
|
||||||
|
EOF
|
||||||
|
echo -e "${GREEN}✓ Created .env file at $ENV_FILE${NC}"
|
||||||
|
echo -e "${YELLOW} Please review and update the .env file with your preferred settings.${NC}\n"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ .env file already exists${NC}\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build Docker images
|
||||||
|
build_images() {
|
||||||
|
echo -e "${YELLOW}Building Docker images (including frontend)...${NC}"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# Check if docker-compose or docker compose
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
else
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$COMPOSE_CMD -f docker/docker-compose.prod.yml build
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Docker images built successfully${NC}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
init_database() {
|
||||||
|
echo -e "${YELLOW}Initializing database...${NC}"
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
echo "Waiting for PostgreSQL to start..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# The SQL files in scripts/init_database/ will be automatically executed
|
||||||
|
# by PostgreSQL's docker-entrypoint-initdb.d mechanism
|
||||||
|
# We just need to wait a bit for it to complete
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Database initialization will be handled automatically by PostgreSQL container${NC}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
start_services() {
|
||||||
|
echo -e "${YELLOW}Starting services...${NC}"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# Check if docker-compose or docker compose
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
else
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$COMPOSE_CMD -f docker/docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Services started${NC}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
show_status() {
|
||||||
|
# Load environment variables from .env if it exists
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
set -a
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Installation Complete ===${NC}\n"
|
||||||
|
echo -e "Services are starting up. Please wait a moment for them to be ready.\n"
|
||||||
|
echo -e "Access points:"
|
||||||
|
echo -e " - Frontend: http://localhost:${FRONTEND_PORT:-80}"
|
||||||
|
echo -e " - Backend API: http://localhost:${BACKEND_PORT:-8500}"
|
||||||
|
echo -e " - Database: localhost:${POSTGRES_PORT:-5432}\n"
|
||||||
|
echo -e "To view logs:"
|
||||||
|
echo -e " docker-compose -f docker/docker-compose.prod.yml logs -f\n"
|
||||||
|
echo -e "To stop services:"
|
||||||
|
echo -e " docker-compose -f docker/docker-compose.prod.yml down\n"
|
||||||
|
echo -e "To restart services:"
|
||||||
|
echo -e " docker-compose -f docker/docker-compose.prod.yml restart\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
check_requirements
|
||||||
|
create_env_file
|
||||||
|
build_images
|
||||||
|
start_services
|
||||||
|
init_database
|
||||||
|
show_status
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Waiting for services to be healthy...${NC}"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Load environment variables for health check
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
set -a
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check service health
|
||||||
|
echo -e "\n${YELLOW}Checking service health...${NC}"
|
||||||
|
sleep 5 # Give services a bit more time
|
||||||
|
if curl -f http://localhost:${BACKEND_PORT:-8500}/health &> /dev/null; then
|
||||||
|
echo -e "${GREEN}✓ Backend is healthy${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ Backend is still starting up. Check logs with: docker-compose -f docker/docker-compose.prod.yml logs backend${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main
|
||||||
|
|
||||||
Reference in New Issue
Block a user