polish + cli + helm + standalone
This commit is contained in:
parent
a1c5f9847a
commit
588065182a
138 changed files with 4661 additions and 1658 deletions
21
.env.dev
21
.env.dev
|
|
@ -1,21 +0,0 @@
|
|||
# Development/Production environment
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres.goflmrvzwagridonyxxn:OjUtCVVtoV0nGPPP@aws-1-us-east-1.pooler.supabase.com:6543/postgres
|
||||
|
||||
# Disable tokenizers parallelism warning (happens with forked processes)
|
||||
TOKENIZERS_PARALLELISM=false
|
||||
|
||||
# Main LLM Configuration (for memory operations: put/think/opinions)
|
||||
# Choose one: "openai", "groq", or "ollama"
|
||||
MEMORY_LLM_PROVIDER=groq
|
||||
MEMORY_LLM_API_KEY=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
|
||||
MEMORY_LLM_MODEL=openai/gpt-oss-120b
|
||||
# MEMORY_LLM_BASE_URL=http://localhost:11434/v1 # For ollama or custom endpoints
|
||||
|
||||
# Judge LLM Configuration (for benchmark evaluation)
|
||||
# If not set, falls back to main LLM configuration
|
||||
JUDGE_LLM_PROVIDER=groq
|
||||
JUDGE_LLM_API_KEY=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
|
||||
JUDGE_LLM_MODEL=openai/gpt-oss-120b
|
||||
# JUDGE_LLM_BASE_URL=https://api.custom.com/v1 # Optional custom endpoint
|
||||
39
.env.example
Normal file
39
.env.example
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# =============================================================================
|
||||
# MEMORA ENVIRONMENT CONFIGURATION
|
||||
# =============================================================================
|
||||
# Copy this file to .env and update with your values
|
||||
# Both services (API and Control Plane) read from this single file
|
||||
|
||||
# =============================================================================
|
||||
# API SERVICE (MEMORA_API_*)
|
||||
# =============================================================================
|
||||
|
||||
# Database
|
||||
MEMORA_API_DATABASE_URL=postgresql://memora:memora_dev@localhost:5432/memora
|
||||
|
||||
# LLM Provider: "openai", "groq", or "ollama"
|
||||
MEMORA_API_LLM_PROVIDER=groq
|
||||
|
||||
# LLM Model (provider-specific)
|
||||
MEMORA_API_LLM_MODEL=openai/gpt-oss-20b
|
||||
|
||||
# API Key (not needed for ollama)
|
||||
MEMORA_API_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# Optional: Custom base URL (for ollama or custom endpoints)
|
||||
# MEMORA_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
|
||||
# API Server Configuration (optional)
|
||||
# MEMORA_API_HOST=0.0.0.0
|
||||
# MEMORA_API_PORT=8080
|
||||
|
||||
# =============================================================================
|
||||
# CONTROL PLANE SERVICE (MEMORA_CP_*)
|
||||
# =============================================================================
|
||||
|
||||
# Dataplane API URL (where the control plane connects to)
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
|
||||
# Control Plane Server Configuration (optional)
|
||||
# MEMORA_CP_PORT=3000
|
||||
# MEMORA_CP_HOSTNAME=0.0.0.0
|
||||
225
.github/workflows/release.yml
vendored
Normal file
225
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
name: Build Release Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-python-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: [memora, benchmarks, memora-dev]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd ${{ matrix.package }}
|
||||
uv build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-${{ matrix.package }}-dist
|
||||
path: ${{ matrix.package }}/dist/*
|
||||
retention-days: 30
|
||||
|
||||
build-rust-cli:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact_name: memora
|
||||
asset_name: memora-linux-amd64
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
artifact_name: memora
|
||||
asset_name: memora-darwin-amd64
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
artifact_name: memora
|
||||
asset_name: memora-darwin-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: memora-cli/target
|
||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
working-directory: memora-cli
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare artifact
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp memora-cli/target/${{ matrix.target }}/release/${{ matrix.artifact_name }} artifacts/${{ matrix.asset_name }}
|
||||
chmod +x artifacts/${{ matrix.asset_name }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rust-cli-${{ matrix.asset_name }}
|
||||
path: artifacts/${{ matrix.asset_name }}
|
||||
retention-days: 30
|
||||
|
||||
build-control-plane:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: memora-control-plane/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./memora-control-plane
|
||||
run: npm ci
|
||||
|
||||
- name: Build Next.js app
|
||||
working-directory: ./memora-control-plane
|
||||
run: npm run build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: control-plane-build
|
||||
path: |
|
||||
memora-control-plane/.next/standalone
|
||||
memora-control-plane/.next/static
|
||||
retention-days: 30
|
||||
|
||||
build-docker-images:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
component: [standalone, control-plane]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract version from tag
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Docker image (standalone)
|
||||
if: matrix.component == 'standalone'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: standalone/Dockerfile
|
||||
push: false
|
||||
tags: memora-standalone:${{ steps.get_version.outputs.VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/memora-standalone.tar
|
||||
|
||||
- name: Build Docker image (control-plane)
|
||||
if: matrix.component == 'control-plane'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./memora-control-plane
|
||||
file: memora-control-plane/Dockerfile
|
||||
push: false
|
||||
tags: memora-control-plane:${{ steps.get_version.outputs.VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/memora-control-plane.tar
|
||||
|
||||
- name: Upload Docker image artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-image-${{ matrix.component }}
|
||||
path: /tmp/memora-${{ matrix.component }}.tar
|
||||
retention-days: 30
|
||||
|
||||
package-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Lint Helm chart
|
||||
run: |
|
||||
helm lint helm/memora
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
helm package helm/memora --destination ./helm-packages
|
||||
|
||||
- name: Upload Helm chart artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: helm-chart
|
||||
path: helm-packages/*.tgz
|
||||
retention-days: 30
|
||||
|
||||
create-release-summary:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-python-packages, build-rust-cli, build-control-plane, build-docker-images, package-helm-chart]
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Create release summary
|
||||
run: |
|
||||
echo "# Release Artifacts Built Successfully" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Components" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Python packages (memora, benchmarks, memora-dev)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Rust CLI (Linux amd64, macOS amd64, macOS arm64)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Control Plane Next.js application" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Docker images (standalone, control-plane)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Helm chart" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All artifacts are available for download in the workflow artifacts." >> $GITHUB_STEP_SUMMARY
|
||||
56
.github/workflows/test.yml
vendored
Normal file
56
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: memora_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
env:
|
||||
MEMORA_API_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/memora_test
|
||||
MEMORA_API_LLM_PROVIDER: groq
|
||||
MEMORA_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
MEMORA_API_LLM_MODEL: openai/gpt-oss-120b
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run migrations
|
||||
working-directory: ./memora
|
||||
run: |
|
||||
uv run alembic upgrade head
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest memora/tests -v
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,7 +11,6 @@ wheels/
|
|||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
|
|
|||
|
|
@ -201,39 +201,28 @@ Raw content is processed through an LLM (Groq by default) to extract meaningful
|
|||
uv sync
|
||||
```
|
||||
|
||||
2. Configure environment files:
|
||||
2. Configure environment file:
|
||||
|
||||
Create `.env.local` for local development:
|
||||
Create `.env` file:
|
||||
```bash
|
||||
cat > .env.local << 'EOF'
|
||||
# Database
|
||||
DATABASE_URL=postgresql://memora:memora_dev@localhost:5432/memora
|
||||
cat > .env << 'EOF'
|
||||
# API Service Configuration
|
||||
MEMORA_API_DATABASE_URL=postgresql://memora:memora_dev@localhost:5432/memora
|
||||
|
||||
# LLM Provider: "openai", "groq", or "ollama"
|
||||
LLM_PROVIDER=groq
|
||||
MEMORA_API_LLM_PROVIDER=groq
|
||||
|
||||
# API Key (not needed for ollama)
|
||||
LLM_API_KEY=your_api_key_here
|
||||
MEMORA_API_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# LLM Model
|
||||
MEMORA_API_LLM_MODEL=openai/gpt-oss-120b
|
||||
|
||||
# Optional: Custom base URL (for ollama or custom endpoints)
|
||||
# LLM_BASE_URL=http://localhost:11434/v1
|
||||
EOF
|
||||
```
|
||||
# MEMORA_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
|
||||
Create `.env.dev` for dev/production environment:
|
||||
```bash
|
||||
cat > .env.dev << 'EOF'
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@host:5432/memora
|
||||
|
||||
# LLM Provider: "openai", "groq", or "ollama"
|
||||
LLM_PROVIDER=groq
|
||||
|
||||
# API Key (not needed for ollama)
|
||||
LLM_API_KEY=your_api_key_here
|
||||
|
||||
# Optional: Custom base URL
|
||||
# LLM_BASE_URL=https://api.custom-provider.com/v1
|
||||
# Control Plane Configuration
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
EOF
|
||||
```
|
||||
|
||||
|
|
@ -286,17 +275,23 @@ JUDGE_LLM_API_KEY=your_openai_key
|
|||
### Local Development
|
||||
|
||||
```bash
|
||||
# Start local PostgreSQL (with initialization)
|
||||
./scripts/start-local-db.sh
|
||||
# Start all services with Docker (PostgreSQL, API, Control Plane)
|
||||
cd ../docker
|
||||
./start.sh
|
||||
|
||||
# Start the server with local environment (default)
|
||||
# Or start services individually:
|
||||
# 1. Start PostgreSQL only
|
||||
# (then migrations run automatically when API starts)
|
||||
# 2. Start the server with local environment
|
||||
./scripts/start-server.sh --env local
|
||||
|
||||
# Start the server with dev environment
|
||||
./scripts/start-server.sh --env dev
|
||||
# Stop all Docker services
|
||||
cd ../docker
|
||||
./stop.sh
|
||||
|
||||
# Erase local database (stop + cleanup)
|
||||
./scripts/erase-local-db.sh
|
||||
# Erase all data and containers
|
||||
cd ../docker
|
||||
./clean.sh
|
||||
```
|
||||
|
||||
The server will start at http://localhost:8080
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Dataplane API Configuration
|
||||
# URL of the Python FastAPI dataplane server (server-side only, not exposed to browser)
|
||||
DATAPLANE_API_URL=http://localhost:8080
|
||||
175
docker/README.md
Normal file
175
docker/README.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Memora Docker Setup
|
||||
|
||||
Complete Docker Compose setup for running all Memora services locally.
|
||||
|
||||
## Services
|
||||
|
||||
This setup includes:
|
||||
- **PostgreSQL** with pgvector extension (port 5432)
|
||||
- **API Service** - FastAPI backend (port 8080)
|
||||
- **Control Plane** - Next.js web UI (port 3000)
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Configure environment variables:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env and set your API keys
|
||||
```
|
||||
|
||||
2. **Start all services:**
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
3. **Access the services:**
|
||||
- Control Plane: http://localhost:3000
|
||||
- API: http://localhost:8080
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
## Scripts
|
||||
|
||||
### `./start.sh`
|
||||
Build and start all services. Waits for all services to be healthy.
|
||||
|
||||
### `./stop.sh`
|
||||
Stop all services (keeps data).
|
||||
|
||||
### `./clean.sh`
|
||||
Stop all services and remove all data (destructive).
|
||||
|
||||
### `./logs.sh [service]`
|
||||
View logs for all services or a specific service:
|
||||
```bash
|
||||
./logs.sh # All services
|
||||
./logs.sh api # API only
|
||||
./logs.sh postgres # PostgreSQL only
|
||||
./logs.sh control-plane # Control plane only
|
||||
```
|
||||
|
||||
## Manual Docker Compose Commands
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
|
||||
# Rebuild and start
|
||||
docker-compose up --build -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Remove everything including data
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
### Connection Info
|
||||
- **Host:** localhost
|
||||
- **Port:** 5432
|
||||
- **Database:** memora
|
||||
- **User:** memora
|
||||
- **Password:** memora_dev
|
||||
|
||||
### Migrations
|
||||
|
||||
Database migrations run automatically when the API service starts. The API uses Alembic to:
|
||||
1. Check the current schema version
|
||||
2. Run any pending migrations
|
||||
3. Initialize the database if it's empty
|
||||
|
||||
Extensions (pgvector, uuid-ossp) are created automatically by the first migration.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required in `.env` file:
|
||||
|
||||
```bash
|
||||
# API Service Configuration
|
||||
MEMORA_API_DATABASE_URL=postgresql://memora:memora_dev@localhost:5432/memora
|
||||
MEMORA_API_LLM_PROVIDER=groq
|
||||
MEMORA_API_LLM_API_KEY=your-api-key-here
|
||||
MEMORA_API_LLM_MODEL=openai/gpt-oss-120b
|
||||
|
||||
# Optional: Custom LLM endpoint
|
||||
# MEMORA_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
|
||||
# Control Plane Configuration
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Services won't start
|
||||
Check logs for errors:
|
||||
```bash
|
||||
./logs.sh
|
||||
```
|
||||
|
||||
### Database connection issues
|
||||
Ensure PostgreSQL is healthy:
|
||||
```bash
|
||||
docker exec memora-postgres pg_isready -U memora
|
||||
```
|
||||
|
||||
### API won't connect to database
|
||||
Check if migrations ran successfully:
|
||||
```bash
|
||||
./logs.sh api
|
||||
```
|
||||
|
||||
### Control plane can't reach API
|
||||
Verify the API is running:
|
||||
```bash
|
||||
curl http://localhost:8080/
|
||||
```
|
||||
|
||||
### Reset everything
|
||||
```bash
|
||||
./clean.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Rebuilding after code changes
|
||||
|
||||
**API changes:**
|
||||
```bash
|
||||
docker-compose up --build -d api
|
||||
```
|
||||
|
||||
**Control Plane changes:**
|
||||
```bash
|
||||
docker-compose up --build -d control-plane
|
||||
```
|
||||
|
||||
### Accessing the database
|
||||
```bash
|
||||
docker exec -it memora-postgres psql -U memora -d memora
|
||||
```
|
||||
|
||||
### Inspecting containers
|
||||
```bash
|
||||
docker-compose ps
|
||||
docker-compose exec api bash
|
||||
docker-compose exec control-plane sh
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
PostgreSQL data is persisted in a Docker volume named `postgres_data`. This data survives container restarts but not `docker-compose down -v`.
|
||||
|
||||
To backup data:
|
||||
```bash
|
||||
docker exec memora-postgres pg_dump -U memora memora > backup.sql
|
||||
```
|
||||
|
||||
To restore data:
|
||||
```bash
|
||||
docker exec -i memora-postgres psql -U memora memora < backup.sql
|
||||
```
|
||||
27
docker/api.Dockerfile
Normal file
27
docker/api.Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy project files
|
||||
COPY memora /app/memora
|
||||
|
||||
# Install Python dependencies
|
||||
WORKDIR /app/memora
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
# Expose API port
|
||||
EXPOSE 8080
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV DATABASE_URL=postgresql://memora:memora_dev@postgres:5432/memora
|
||||
|
||||
# Run the API server
|
||||
CMD ["python", "-m", "memora.web.server", "--host", "0.0.0.0", "--port", "8080"]
|
||||
27
docker/clean.sh
Executable file
27
docker/clean.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🧹 Cleaning Memora Services"
|
||||
echo "============================"
|
||||
echo ""
|
||||
echo "This will:"
|
||||
echo " - Stop all services"
|
||||
echo " - Remove containers"
|
||||
echo " - Remove volumes (ALL DATA WILL BE LOST)"
|
||||
echo ""
|
||||
read -p "Are you sure? (yes/no): " confirm
|
||||
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🗑️ Removing services and data..."
|
||||
docker-compose down -v
|
||||
|
||||
echo ""
|
||||
echo "✅ All services and data removed"
|
||||
echo ""
|
||||
79
docker/docker-compose.yml
Normal file
79
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: memora-postgres
|
||||
environment:
|
||||
POSTGRES_USER: memora
|
||||
POSTGRES_PASSWORD: memora_dev
|
||||
POSTGRES_DB: memora
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U memora"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- memora-network
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/api.Dockerfile
|
||||
container_name: memora-api
|
||||
environment:
|
||||
MEMORA_API_DATABASE_URL: postgresql://memora:memora_dev@postgres:5432/memora
|
||||
MEMORA_API_LLM_PROVIDER: ${MEMORA_API_LLM_PROVIDER:-groq}
|
||||
MEMORA_API_LLM_API_KEY: ${MEMORA_API_LLM_API_KEY}
|
||||
MEMORA_API_LLM_MODEL: ${MEMORA_API_LLM_MODEL:-openai/gpt-oss-120b}
|
||||
MEMORA_API_LLM_BASE_URL: ${MEMORA_API_LLM_BASE_URL}
|
||||
MEMORA_API_HOST: ${MEMORA_API_HOST:-0.0.0.0}
|
||||
MEMORA_API_PORT: ${MEMORA_API_PORT:-8080}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- memora-network
|
||||
restart: unless-stopped
|
||||
|
||||
control-plane:
|
||||
build:
|
||||
context: ../memora-control-plane
|
||||
dockerfile: ../docker/control-plane.Dockerfile
|
||||
container_name: memora-control-plane
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
MEMORA_CP_HOSTNAME: ${MEMORA_CP_HOSTNAME:-0.0.0.0}
|
||||
MEMORA_CP_PORT: ${MEMORA_CP_PORT:-3000}
|
||||
MEMORA_CP_DATAPLANE_API_URL: ${MEMORA_CP_DATAPLANE_API_URL:-http://api:8080}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- memora-network
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
memora-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
16
docker/logs.sh
Executable file
16
docker/logs.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SERVICE=$1
|
||||
|
||||
if [ -z "$SERVICE" ]; then
|
||||
echo "📋 Showing logs for all services..."
|
||||
echo ""
|
||||
docker-compose logs -f
|
||||
else
|
||||
echo "📋 Showing logs for $SERVICE..."
|
||||
echo ""
|
||||
docker-compose logs -f "$SERVICE"
|
||||
fi
|
||||
65
docker/start.sh
Executable file
65
docker/start.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🚀 Starting Memora Services"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
# Check if .env file exists in root
|
||||
if [ ! -f ../.env ]; then
|
||||
echo "⚠️ No .env file found in project root!"
|
||||
echo ""
|
||||
echo "Creating .env from .env.example..."
|
||||
cp ../.env.example ../.env
|
||||
echo ""
|
||||
echo "⚠️ Please edit .env and set your API keys:"
|
||||
echo " - MEMORY_LLM_API_KEY"
|
||||
echo ""
|
||||
echo "Then run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Building and starting services..."
|
||||
docker-compose --env-file ../.env up --build -d
|
||||
|
||||
echo ""
|
||||
echo "⏳ Waiting for services to be healthy..."
|
||||
echo ""
|
||||
|
||||
# Wait for PostgreSQL
|
||||
echo " Waiting for PostgreSQL..."
|
||||
until docker exec memora-postgres pg_isready -U memora > /dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
echo " ✅ PostgreSQL is ready"
|
||||
|
||||
# Wait for API
|
||||
echo " Waiting for API..."
|
||||
until curl -f http://localhost:8080/ > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
echo " ✅ API is ready"
|
||||
|
||||
# Wait for Control Plane
|
||||
echo " Waiting for Control Plane..."
|
||||
until curl -f http://localhost:3000/ > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
echo " ✅ Control Plane is ready"
|
||||
|
||||
echo ""
|
||||
echo "✅ All services are running!"
|
||||
echo ""
|
||||
echo "📊 Service URLs:"
|
||||
echo " Control Plane: http://localhost:3000"
|
||||
echo " API: http://localhost:8080"
|
||||
echo " PostgreSQL: localhost:5432"
|
||||
echo ""
|
||||
echo "🔍 View logs:"
|
||||
echo " docker-compose logs -f"
|
||||
echo ""
|
||||
echo "🛑 Stop services:"
|
||||
echo " ./stop.sh"
|
||||
echo ""
|
||||
17
docker/stop.sh
Executable file
17
docker/stop.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🛑 Stopping Memora Services"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
docker-compose down
|
||||
|
||||
echo ""
|
||||
echo "✅ All services stopped"
|
||||
echo ""
|
||||
echo "💡 To remove data volumes as well, run:"
|
||||
echo " docker-compose down -v"
|
||||
echo ""
|
||||
135
helm/INSTALL.txt
Normal file
135
helm/INSTALL.txt
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
MEMORA HELM CHART INSTALLATION GUIDE
|
||||
=====================================
|
||||
|
||||
PREREQUISITES
|
||||
-------------
|
||||
- Kubernetes cluster (1.19+)
|
||||
- kubectl configured
|
||||
- Helm 3.x installed
|
||||
- PostgreSQL database with pgvector extension (if not using bundled PostgreSQL)
|
||||
|
||||
BASIC INSTALLATION
|
||||
------------------
|
||||
|
||||
1. Install with default values (requires external PostgreSQL):
|
||||
|
||||
helm install memora ./memora \
|
||||
--set postgresql.external.host=your-postgres-host \
|
||||
--set postgresql.external.password=your-password \
|
||||
--set api.secrets.MEMORY_LLM_API_KEY=your-api-key
|
||||
|
||||
2. Install with custom values file:
|
||||
|
||||
helm install memora ./memora -f memora/values-production.yaml
|
||||
|
||||
3. Install in a specific namespace:
|
||||
|
||||
kubectl create namespace memora
|
||||
helm install memora ./memora -n memora
|
||||
|
||||
CONFIGURATION OPTIONS
|
||||
---------------------
|
||||
|
||||
Development setup (using values-development.yaml):
|
||||
helm install memora ./memora -f memora/values-development.yaml
|
||||
|
||||
Production setup (using values-production.yaml):
|
||||
helm install memora ./memora -f memora/values-production.yaml
|
||||
|
||||
Custom LLM provider:
|
||||
helm install memora ./memora \
|
||||
--set api.env.MEMORY_LLM_PROVIDER=openai \
|
||||
--set api.env.MEMORY_LLM_MODEL=gpt-4 \
|
||||
--set api.secrets.MEMORY_LLM_API_KEY=sk-your-key
|
||||
|
||||
Enable ingress:
|
||||
helm install memora ./memora \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=memora.example.com
|
||||
|
||||
Enable autoscaling:
|
||||
helm install memora ./memora \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=2 \
|
||||
--set autoscaling.maxReplicas=10
|
||||
|
||||
UPGRADE
|
||||
-------
|
||||
|
||||
Upgrade existing installation:
|
||||
helm upgrade memora ./memora
|
||||
|
||||
Upgrade with new values:
|
||||
helm upgrade memora ./memora -f memora/values-production.yaml
|
||||
|
||||
UNINSTALL
|
||||
---------
|
||||
|
||||
Remove the Helm release:
|
||||
helm uninstall memora
|
||||
|
||||
Remove with namespace:
|
||||
helm uninstall memora -n memora
|
||||
|
||||
TESTING
|
||||
-------
|
||||
|
||||
Test the installation with dry-run:
|
||||
helm install memora ./memora --dry-run --debug
|
||||
|
||||
Validate templates:
|
||||
helm template memora ./memora
|
||||
|
||||
Lint the chart:
|
||||
helm lint ./memora
|
||||
|
||||
ACCESSING THE SERVICES
|
||||
----------------------
|
||||
|
||||
Port-forward control plane:
|
||||
kubectl port-forward svc/memora-control-plane 3000:3000
|
||||
|
||||
Port-forward API:
|
||||
kubectl port-forward svc/memora-api 8080:8080
|
||||
|
||||
Get service URLs:
|
||||
helm status memora
|
||||
|
||||
DATABASE INITIALIZATION
|
||||
-----------------------
|
||||
|
||||
NOTE: Database migrations now run automatically when the API service starts.
|
||||
You typically don't need to run migrations manually.
|
||||
|
||||
If you want to pre-initialize the database before deploying (optional):
|
||||
kubectl run memora-init --rm -it --restart=Never \
|
||||
--image=memora/api:latest \
|
||||
--env="DATABASE_URL=postgresql://user:pass@host:5432/memora" \
|
||||
-- python -c "from memora.migrations import run_migrations; run_migrations()"
|
||||
|
||||
TROUBLESHOOTING
|
||||
---------------
|
||||
|
||||
Check pod status:
|
||||
kubectl get pods -l app.kubernetes.io/name=memora
|
||||
|
||||
View logs for API:
|
||||
kubectl logs -l app.kubernetes.io/component=api
|
||||
|
||||
View logs for control plane:
|
||||
kubectl logs -l app.kubernetes.io/component=control-plane
|
||||
|
||||
Describe a pod:
|
||||
kubectl describe pod <pod-name>
|
||||
|
||||
Check configuration:
|
||||
kubectl get configmap memora-config -o yaml
|
||||
kubectl get secret memora-secret -o yaml
|
||||
|
||||
NOTES
|
||||
-----
|
||||
- Make sure PostgreSQL has pgvector extension enabled
|
||||
- Run database migrations before first use
|
||||
- Configure proper resource limits for production
|
||||
- Use external secrets management for production
|
||||
- Enable TLS/SSL for production deployments
|
||||
23
helm/memora/.helmignore
Normal file
23
helm/memora/.helmignore
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
13
helm/memora/Chart.yaml
Normal file
13
helm/memora/Chart.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v2
|
||||
name: memora
|
||||
description: A Helm chart for Memora - temporal-semantic-entity memory system for AI agents
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
- llm
|
||||
- agents
|
||||
maintainers:
|
||||
- name: Memora Team
|
||||
71
helm/memora/templates/NOTES.txt
Normal file
71
helm/memora/templates/NOTES.txt
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
Thank you for installing {{ .Chart.Name }}!
|
||||
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm status {{ .Release.Name }}
|
||||
$ helm get all {{ .Release.Name }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
The application is accessible via the following URL(s):
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}
|
||||
{{- end }}
|
||||
|
||||
{{- else }}
|
||||
|
||||
1. Get the Control Plane URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.controlPlane.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "memora.fullname" . }}-control-plane)
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo "Control Plane URL: http://$NODE_IP:$NODE_PORT"
|
||||
{{- else if contains "LoadBalancer" .Values.controlPlane.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "memora.fullname" . }}-control-plane'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "memora.fullname" . }}-control-plane --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo "Control Plane URL: http://$SERVICE_IP:{{ .Values.controlPlane.service.port }}"
|
||||
{{- else if contains "ClusterIP" .Values.controlPlane.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/component=control-plane,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Control Plane URL: http://127.0.0.1:3000"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
2. Get the API URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.api.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "memora.fullname" . }}-api)
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo "API URL: http://$NODE_IP:$NODE_PORT"
|
||||
{{- else if contains "LoadBalancer" .Values.api.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "memora.fullname" . }}-api'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "memora.fullname" . }}-api --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo "API URL: http://$SERVICE_IP:{{ .Values.api.service.port }}"
|
||||
{{- else if contains "ClusterIP" .Values.api.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/component=api,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "API URL: http://127.0.0.1:8080"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
|
||||
NOTE: You are using an external PostgreSQL database.
|
||||
Please ensure that:
|
||||
1. The database is accessible from the cluster
|
||||
2. The pgvector extension is enabled
|
||||
|
||||
Database migrations run automatically when the API service starts.
|
||||
|
||||
If you want to pre-initialize the database before deploying (optional):
|
||||
kubectl run --namespace {{ .Release.Namespace }} memora-init --rm -it --restart=Never \
|
||||
--image={{ .Values.api.image.repository }}:{{ .Values.api.image.tag }} \
|
||||
--env="DATABASE_URL={{ include "memora.databaseUrl" . }}" \
|
||||
-- python -c "from memora.migrations import run_migrations; run_migrations()"
|
||||
{{- end }}
|
||||
|
||||
For more information, visit: https://github.com/yourusername/memora
|
||||
112
helm/memora/templates/_helpers.tpl
Normal file
112
helm/memora/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "memora.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "memora.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "memora.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "memora.labels" -}}
|
||||
helm.sh/chart: {{ include "memora.chart" . }}
|
||||
{{ include "memora.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "memora.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "memora.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
API labels
|
||||
*/}}
|
||||
{{- define "memora.api.labels" -}}
|
||||
{{ include "memora.labels" . }}
|
||||
app.kubernetes.io/component: api
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
API selector labels
|
||||
*/}}
|
||||
{{- define "memora.api.selectorLabels" -}}
|
||||
{{ include "memora.selectorLabels" . }}
|
||||
app.kubernetes.io/component: api
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Control plane labels
|
||||
*/}}
|
||||
{{- define "memora.controlPlane.labels" -}}
|
||||
{{ include "memora.labels" . }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Control plane selector labels
|
||||
*/}}
|
||||
{{- define "memora.controlPlane.selectorLabels" -}}
|
||||
{{ include "memora.selectorLabels" . }}
|
||||
app.kubernetes.io/component: control-plane
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "memora.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "memora.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Generate database URL
|
||||
*/}}
|
||||
{{- define "memora.databaseUrl" -}}
|
||||
{{- if .Values.databaseUrl }}
|
||||
{{- .Values.databaseUrl }}
|
||||
{{- else if .Values.postgresql.enabled }}
|
||||
{{- printf "postgresql://%s:%s@%s-postgresql:%d/%s" .Values.postgresql.auth.username .Values.postgresql.auth.password (include "memora.fullname" .) (.Values.postgresql.primary.service.port | int) .Values.postgresql.auth.database }}
|
||||
{{- else }}
|
||||
{{- printf "postgresql://%s:$(POSTGRES_PASSWORD)@%s:%d/%s" .Values.postgresql.external.username .Values.postgresql.external.host (.Values.postgresql.external.port | int) .Values.postgresql.external.database }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
API URL for control plane
|
||||
*/}}
|
||||
{{- define "memora.apiUrl" -}}
|
||||
{{- printf "http://%s-api:%d" (include "memora.fullname" .) (.Values.api.service.port | int) }}
|
||||
{{- end }}
|
||||
93
helm/memora/templates/api-deployment.yaml
Normal file
93
helm/memora/templates/api-deployment.yaml
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{{- if .Values.api.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-api
|
||||
labels:
|
||||
{{- include "memora.api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.api.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "memora.api.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "memora.api.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "memora.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: api
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.api.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.api.service.targetPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: MEMORA_API_DATABASE_URL
|
||||
value: {{ include "memora.databaseUrl" . | quote }}
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-secret
|
||||
key: postgres-password
|
||||
{{- end }}
|
||||
- name: MEMORA_API_LLM_PROVIDER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
key: llm-provider
|
||||
- name: MEMORA_API_LLM_MODEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
key: llm-model
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORA_API_LLM_API_KEY") }}
|
||||
- name: MEMORA_API_LLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-secret
|
||||
key: llm-api-key
|
||||
{{- end }}
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORA_API_LLM_BASE_URL") }}
|
||||
- name: MEMORA_API_LLM_BASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-secret
|
||||
key: llm-base-url
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.api.livenessProbe | nindent 10 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.api.readinessProbe | nindent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.api.resources | nindent 10 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
17
helm/memora/templates/api-service.yaml
Normal file
17
helm/memora/templates/api-service.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{{- if .Values.api.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-api
|
||||
labels:
|
||||
{{- include "memora.api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.api.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.api.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "memora.api.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
15
helm/memora/templates/configmap.yaml
Normal file
15
helm/memora/templates/configmap.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "memora.labels" . | nindent 4 }}
|
||||
data:
|
||||
# API configuration
|
||||
llm-provider: {{ .Values.api.env.MEMORA_API_LLM_PROVIDER | quote }}
|
||||
llm-model: {{ .Values.api.env.MEMORA_API_LLM_MODEL | quote }}
|
||||
|
||||
# Control plane configuration
|
||||
node-env: {{ .Values.controlPlane.env.NODE_ENV | quote }}
|
||||
hostname: {{ .Values.controlPlane.env.MEMORA_CP_HOSTNAME | quote }}
|
||||
control-plane-port: {{ .Values.controlPlane.env.MEMORA_CP_PORT | quote }}
|
||||
76
helm/memora/templates/controlplane-deployment.yaml
Normal file
76
helm/memora/templates/controlplane-deployment.yaml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{{- if .Values.controlPlane.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-control-plane
|
||||
labels:
|
||||
{{- include "memora.controlPlane.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.controlPlane.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "memora.controlPlane.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "memora.controlPlane.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "memora.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: control-plane
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.controlPlane.image.repository }}:{{ .Values.controlPlane.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.controlPlane.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.controlPlane.service.targetPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
key: node-env
|
||||
- name: MEMORA_CP_HOSTNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
key: hostname
|
||||
- name: MEMORA_CP_PORT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "memora.fullname" . }}-config
|
||||
key: control-plane-port
|
||||
- name: MEMORA_CP_DATAPLANE_API_URL
|
||||
value: {{ include "memora.apiUrl" . | quote }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.controlPlane.livenessProbe | nindent 10 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.controlPlane.readinessProbe | nindent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.controlPlane.resources | nindent 10 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
17
helm/memora/templates/controlplane-service.yaml
Normal file
17
helm/memora/templates/controlplane-service.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{{- if .Values.controlPlane.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-control-plane
|
||||
labels:
|
||||
{{- include "memora.controlPlane.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.controlPlane.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.controlPlane.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "memora.controlPlane.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
64
helm/memora/templates/hpa.yaml
Normal file
64
helm/memora/templates/hpa.yaml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-api
|
||||
labels:
|
||||
{{- include "memora.api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "memora.fullname" . }}-api
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-control-plane
|
||||
labels:
|
||||
{{- include "memora.controlPlane.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "memora.fullname" . }}-control-plane
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
47
helm/memora/templates/ingress.yaml
Normal file
47
helm/memora/templates/ingress.yaml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}
|
||||
labels:
|
||||
{{- include "memora.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
{{- if eq .service "api" }}
|
||||
name: {{ include "memora.fullname" $ }}-api
|
||||
port:
|
||||
number: {{ $.Values.api.service.port }}
|
||||
{{- else if eq .service "controlPlane" }}
|
||||
name: {{ include "memora.fullname" $ }}-control-plane
|
||||
port:
|
||||
number: {{ $.Values.controlPlane.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
19
helm/memora/templates/secret.yaml
Normal file
19
helm/memora/templates/secret.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "memora.fullname" . }}-secret
|
||||
labels:
|
||||
{{- include "memora.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORY_LLM_API_KEY") }}
|
||||
llm-api-key: {{ .Values.api.secrets.MEMORY_LLM_API_KEY | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORY_LLM_BASE_URL") }}
|
||||
llm-base-url: {{ .Values.api.secrets.MEMORY_LLM_BASE_URL | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
{{- if .Values.postgresql.external.password }}
|
||||
postgres-password: {{ .Values.postgresql.external.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
12
helm/memora/templates/serviceaccount.yaml
Normal file
12
helm/memora/templates/serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "memora.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "memora.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
185
helm/memora/values.yaml
Normal file
185
helm/memora/values.yaml
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Default values for memora
|
||||
|
||||
# Global settings
|
||||
replicaCount: 1
|
||||
|
||||
# Image settings for api
|
||||
api:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: memora/api
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
# Resource limits and requests
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 4Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
# Liveness and readiness probes
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
MEMORA_API_LLM_PROVIDER: "groq"
|
||||
MEMORA_API_LLM_MODEL: "openai/gpt-oss-120b"
|
||||
|
||||
# Secret environment variables
|
||||
secrets:
|
||||
# MEMORA_API_LLM_API_KEY: "your-api-key"
|
||||
# MEMORA_API_LLM_BASE_URL: "https://api.groq.com/openai/v1"
|
||||
|
||||
# Image settings for control plane
|
||||
controlPlane:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: memora/memora-control-plane
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
|
||||
# Resource limits and requests
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
|
||||
# Liveness and readiness probes
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
NODE_ENV: "production"
|
||||
MEMORA_CP_HOSTNAME: "0.0.0.0"
|
||||
MEMORA_CP_PORT: "3000"
|
||||
|
||||
# PostgreSQL configuration
|
||||
postgresql:
|
||||
# Set to true to deploy PostgreSQL as part of this chart
|
||||
enabled: false
|
||||
|
||||
# External PostgreSQL connection details
|
||||
# If postgresql.enabled is false, provide external database details
|
||||
external:
|
||||
host: "postgresql"
|
||||
port: 5432
|
||||
database: "memora"
|
||||
username: "memora"
|
||||
# Password should be provided via secret
|
||||
# password: ""
|
||||
|
||||
# Database URL (auto-generated from postgresql config if not provided)
|
||||
# databaseUrl: "postgresql://user:pass@host:5432/database"
|
||||
|
||||
# Ingress configuration
|
||||
ingress:
|
||||
enabled: false
|
||||
className: "nginx"
|
||||
annotations: {}
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
|
||||
hosts:
|
||||
- host: memora.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
service: controlPlane
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api
|
||||
|
||||
tls: []
|
||||
# - secretName: memora-tls
|
||||
# hosts:
|
||||
# - memora.example.com
|
||||
|
||||
# Service Account
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
# Pod annotations
|
||||
podAnnotations: {}
|
||||
|
||||
# Pod security context
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
|
||||
# Security context
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
# Node selector
|
||||
nodeSelector: {}
|
||||
|
||||
# Tolerations
|
||||
tolerations: []
|
||||
|
||||
# Affinity
|
||||
affinity: {}
|
||||
|
||||
# Autoscaling
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
6
local-db/.gitignore
vendored
6
local-db/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
|||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Docker volumes (data persistence)
|
||||
postgres_data/
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: memora-postgres
|
||||
environment:
|
||||
POSTGRES_USER: memora
|
||||
POSTGRES_PASSWORD: memora_dev
|
||||
POSTGRES_DB: memora
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-extensions.sql:/docker-entrypoint-initdb.d/01-init-extensions.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U memora"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- Enable pgvector extension
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
102
memora-cli/.github/workflows/release.yml
vendored
Normal file
102
memora-cli/.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact_name: memora
|
||||
release_name: memora-linux-x86_64
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
artifact_name: memora
|
||||
release_name: memora-linux-arm64
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
artifact_name: memora
|
||||
release_name: memora-macos-x86_64
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
artifact_name: memora
|
||||
release_name: memora-macos-arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install cross-compilation tools (Linux ARM64)
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Strip binary (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Strip binary (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.release_name }}
|
||||
path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create checksums
|
||||
run: |
|
||||
cd artifacts
|
||||
for dir in */; do
|
||||
cd "$dir"
|
||||
sha256sum * > SHA256SUMS
|
||||
cd ..
|
||||
done
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
artifacts/memora-linux-x86_64/memora
|
||||
artifacts/memora-linux-arm64/memora
|
||||
artifacts/memora-macos-x86_64/memora
|
||||
artifacts/memora-macos-arm64/memora
|
||||
artifacts/*/SHA256SUMS
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
20
memora-cli/.gitignore
vendored
Normal file
20
memora-cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Rust
|
||||
/target/
|
||||
Cargo.lock
|
||||
|
||||
# Distribution
|
||||
/dist/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
47
memora-cli/Cargo.toml
Normal file
47
memora-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "memora-cli-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Memora Team"]
|
||||
description = "A beautiful CLI for Memora - semantic memory system"
|
||||
license = "MIT"
|
||||
|
||||
[[bin]]
|
||||
name = "memora"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# CLI framework
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
# TUI libraries
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
|
||||
# Colors and styling
|
||||
colored = "2.1"
|
||||
indicatif = "0.17"
|
||||
|
||||
# Error handling
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
# Utilities
|
||||
chrono = "0.4"
|
||||
walkdir = "2.5"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Memora CLI
|
||||
|
||||
Modern command-line interface for the Memora Temporal Semantic Memory System.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install memora-cli
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the API endpoint URL (defaults to `http://localhost:8080`):
|
||||
|
||||
```bash
|
||||
export MEMORA_API_URL="http://localhost:8080"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Search Memories
|
||||
|
||||
```bash
|
||||
memora search alice "What did she say about AI?"
|
||||
memora search alice "hiking activities" --type world --max-tokens 8000
|
||||
memora search alice "recent events" --budget 150 --trace
|
||||
```
|
||||
|
||||
### Think (Generate Answers)
|
||||
|
||||
```bash
|
||||
memora think alice "What do you think about machine learning?"
|
||||
```
|
||||
|
||||
### Store Memories
|
||||
|
||||
Store a single memory:
|
||||
|
||||
```bash
|
||||
memora put alice "Alice loves machine learning and AI"
|
||||
memora put alice "Today we discussed neural networks" --context "team meeting"
|
||||
|
||||
# Async mode - returns immediately, processes in background
|
||||
memora put alice "Important note" --async
|
||||
```
|
||||
|
||||
### Import Files
|
||||
|
||||
Import memories from local files (.txt and .md):
|
||||
|
||||
```bash
|
||||
# Import a single file
|
||||
memora put-files alice meeting-notes.txt
|
||||
|
||||
# Import all files from a directory
|
||||
memora put-files alice ./documents/
|
||||
|
||||
# Async mode - queue files for background processing
|
||||
memora put-files alice ./documents/ --async
|
||||
```
|
||||
|
||||
### List Agents
|
||||
|
||||
```bash
|
||||
memora agents
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Beautiful TUI with Rich formatting (panels, tables, syntax highlighting)
|
||||
- Color-coded fact types (cyan=world, magenta=agent, yellow=opinion)
|
||||
- Progress bars and spinners for async operations
|
||||
- Tree views for file hierarchies
|
||||
- HTTP client (no direct database access needed)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.11
|
||||
- Memora API server running
|
||||
100
memora-cli/build.sh
Executable file
100
memora-cli/build.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Build script for memora-cli-rust
|
||||
# This script builds optimized binaries for multiple platforms
|
||||
|
||||
# Source cargo environment if it exists
|
||||
if [ -f "$HOME/.cargo/env" ]; then
|
||||
source "$HOME/.cargo/env"
|
||||
fi
|
||||
|
||||
# Add cargo to PATH if not already there
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# Check if cargo is available
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "Error: Cargo not found. Please install Rust first:"
|
||||
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building Memora CLI for multiple platforms..."
|
||||
|
||||
# Ensure we're in the right directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Create dist directory if it doesn't exist
|
||||
mkdir -p dist
|
||||
|
||||
# Get version from Cargo.toml
|
||||
VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)
|
||||
echo "Version: $VERSION"
|
||||
|
||||
# Function to build for a target
|
||||
build_target() {
|
||||
local target=$1
|
||||
local output_name=$2
|
||||
|
||||
echo ""
|
||||
echo "Building for $target..."
|
||||
|
||||
# Check if target is installed, install if not
|
||||
if ! rustup target list | grep -q "$target (installed)"; then
|
||||
echo "Installing target $target..."
|
||||
rustup target add "$target"
|
||||
fi
|
||||
|
||||
# Build
|
||||
cargo build --release --target "$target"
|
||||
|
||||
# Copy to dist
|
||||
if [[ "$target" == *"windows"* ]]; then
|
||||
cp "target/$target/release/memora.exe" "dist/$output_name.exe"
|
||||
echo "Created: dist/$output_name.exe"
|
||||
else
|
||||
cp "target/$target/release/memora" "dist/$output_name"
|
||||
chmod +x "dist/$output_name"
|
||||
echo "Created: dist/$output_name"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect current platform
|
||||
OS=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
|
||||
echo "Detected platform: $OS $ARCH"
|
||||
|
||||
# Build for current platform
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
echo "Building for macOS ARM64 (Apple Silicon)..."
|
||||
build_target "aarch64-apple-darwin" "memora-macos-arm64"
|
||||
else
|
||||
echo "Building for macOS x86_64 (Intel)..."
|
||||
build_target "x86_64-apple-darwin" "memora-macos-x86_64"
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
if [[ "$ARCH" == "x86_64" ]]; then
|
||||
echo "Building for Linux x86_64..."
|
||||
build_target "x86_64-unknown-linux-gnu" "memora-linux-x86_64"
|
||||
elif [[ "$ARCH" == "aarch64" ]]; then
|
||||
echo "Building for Linux ARM64..."
|
||||
build_target "aarch64-unknown-linux-gnu" "memora-linux-arm64"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported OS: $OS"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Build complete! Binaries are in the dist/ directory:"
|
||||
ls -lh dist/
|
||||
|
||||
echo ""
|
||||
echo "To build for other platforms, run:"
|
||||
echo " ./build.sh --all"
|
||||
158
memora-cli/install.sh
Executable file
158
memora-cli/install.sh
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Memora CLI installer
|
||||
# Usage: curl -sSf https://your-domain.com/install.sh | sh
|
||||
|
||||
REPO_URL="https://github.com/your-org/memora-cli"
|
||||
INSTALL_DIR="${MEMORA_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
BINARY_NAME="memora"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
print_banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ MEMORA CLI INSTALLER ║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Detect platform
|
||||
detect_platform() {
|
||||
local os=$(uname -s)
|
||||
local arch=$(uname -m)
|
||||
|
||||
case "$os" in
|
||||
Darwin)
|
||||
if [[ "$arch" == "arm64" ]] || [[ "$arch" == "aarch64" ]]; then
|
||||
echo "macos-arm64"
|
||||
elif [[ "$arch" == "x86_64" ]]; then
|
||||
echo "macos-x86_64"
|
||||
else
|
||||
print_error "Unsupported macOS architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
echo "linux-x86_64"
|
||||
elif [[ "$arch" == "aarch64" ]] || [[ "$arch" == "arm64" ]]; then
|
||||
echo "linux-arm64"
|
||||
else
|
||||
print_error "Unsupported Linux architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
print_error "Unsupported operating system: $os"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Download binary
|
||||
download_binary() {
|
||||
local platform=$1
|
||||
local download_url="${REPO_URL}/releases/latest/download/memora-${platform}"
|
||||
local tmp_file="/tmp/memora-$$"
|
||||
|
||||
print_info "Downloading Memora CLI for $platform..."
|
||||
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -fsSL "$download_url" -o "$tmp_file"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -q "$download_url" -O "$tmp_file"
|
||||
else
|
||||
print_error "Neither curl nor wget found. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$tmp_file"
|
||||
}
|
||||
|
||||
# Install binary
|
||||
install_binary() {
|
||||
local tmp_file=$1
|
||||
|
||||
# Create install directory if it doesn't exist
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Move binary to install directory
|
||||
mv "$tmp_file" "$INSTALL_DIR/$BINARY_NAME"
|
||||
chmod +x "$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
print_success "Installed to: $INSTALL_DIR/$BINARY_NAME"
|
||||
}
|
||||
|
||||
# Check if directory is in PATH
|
||||
check_path() {
|
||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
print_warning "$INSTALL_DIR is not in your PATH"
|
||||
echo ""
|
||||
echo "Add it to your PATH by adding this line to your shell profile:"
|
||||
echo ""
|
||||
|
||||
# Detect shell
|
||||
if [[ -n "$BASH_VERSION" ]]; then
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.bashrc"
|
||||
echo " source ~/.bashrc"
|
||||
elif [[ -n "$ZSH_VERSION" ]]; then
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc"
|
||||
echo " source ~/.zshrc"
|
||||
else
|
||||
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
main() {
|
||||
print_banner
|
||||
|
||||
# Detect platform
|
||||
platform=$(detect_platform)
|
||||
print_info "Detected platform: $platform"
|
||||
|
||||
# Download binary
|
||||
tmp_file=$(download_binary "$platform")
|
||||
|
||||
# Install binary
|
||||
install_binary "$tmp_file"
|
||||
|
||||
# Check PATH
|
||||
check_path
|
||||
|
||||
print_success "Installation complete!"
|
||||
echo ""
|
||||
print_info "Try it out: $BINARY_NAME --help"
|
||||
echo ""
|
||||
print_info "Configure the API URL:"
|
||||
echo " export MEMORA_API_URL=http://localhost:8080"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run installation
|
||||
main
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
"""
|
||||
Memora CLI - Modern command-line interface for the Memora Memory System.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
|
@ -1,565 +0,0 @@
|
|||
"""
|
||||
Memora CLI - HTTP client for Memora API.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
||||
from rich.markdown import Markdown
|
||||
from rich import box
|
||||
from rich.tree import Tree
|
||||
|
||||
app = typer.Typer(
|
||||
name="memora",
|
||||
help="Modern CLI for Memora - Temporal Semantic Memory System",
|
||||
add_completion=False,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def get_api_url():
|
||||
"""Get API URL from environment variable."""
|
||||
api_url = os.getenv("MEMORA_API_URL", "http://localhost:8080")
|
||||
return api_url.rstrip("/")
|
||||
|
||||
|
||||
def make_api_request(
|
||||
method: str,
|
||||
endpoint: str,
|
||||
json_data: Optional[dict] = None,
|
||||
timeout: float = 60.0,
|
||||
) -> dict:
|
||||
"""
|
||||
Make an API request with proper error handling.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: API endpoint path (e.g., "/api/search")
|
||||
json_data: Optional JSON payload for POST requests
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Response data as dict
|
||||
|
||||
Raises:
|
||||
typer.Exit on any error
|
||||
"""
|
||||
api_url = get_api_url()
|
||||
full_url = f"{api_url}{endpoint}"
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
if method.upper() == "GET":
|
||||
response = client.get(full_url)
|
||||
elif method.upper() == "POST":
|
||||
response = client.post(full_url, json=json_data)
|
||||
else:
|
||||
console.print(f"[red]Error: Unsupported HTTP method: {method}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Check HTTP status
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
|
||||
# Check for success field in response (if present)
|
||||
if "success" in data and not data["success"]:
|
||||
error_msg = data.get("message", "Unknown error")
|
||||
console.print(f"[red]API Error: {error_msg}[/red]")
|
||||
if "detail" in data:
|
||||
console.print(f"[yellow]Details: {data['detail']}[/yellow]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
return data
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
console.print(f"[red]HTTP Error {e.response.status_code}[/red]")
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "detail" in error_data:
|
||||
console.print(f"[red]Error: {error_data['detail']}[/red]")
|
||||
else:
|
||||
console.print(f"[red]Error: {error_data}[/red]")
|
||||
except Exception:
|
||||
console.print(f"[red]Error: {e.response.text}[/red]")
|
||||
console.print(f"[yellow]Make sure the API is running at {api_url}[/yellow]")
|
||||
raise typer.Exit(1)
|
||||
except httpx.ConnectError as e:
|
||||
console.print(f"[red]Connection Error: Failed to connect to API at {api_url}[/red]")
|
||||
console.print(f"[yellow]Make sure the API server is running[/yellow]")
|
||||
raise typer.Exit(1)
|
||||
except httpx.TimeoutException:
|
||||
console.print(f"[red]Timeout Error: Request took too long[/red]")
|
||||
console.print(f"[yellow]Try increasing the timeout or check the API server[/yellow]")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Unexpected Error: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def search(
|
||||
agent_id: str = typer.Argument(..., help="Agent ID to search for"),
|
||||
query: str = typer.Argument(..., help="Search query"),
|
||||
fact_type: List[str] = typer.Option(
|
||||
["world", "agent", "opinion"],
|
||||
"--type",
|
||||
"-t",
|
||||
help="Fact types to search (world/agent/opinion)",
|
||||
),
|
||||
thinking_budget: int = typer.Option(
|
||||
100, "--budget", "-b", help="Thinking budget for search"
|
||||
),
|
||||
max_tokens: int = typer.Option(
|
||||
4096, "--max-tokens", help="Maximum tokens for search results"
|
||||
),
|
||||
trace: bool = typer.Option(False, "--trace", help="Show trace information"),
|
||||
):
|
||||
"""
|
||||
Search memory using semantic similarity.
|
||||
|
||||
Example:
|
||||
memora search alice "What did she say about AI?"
|
||||
"""
|
||||
with console.status(f"[bold blue]Searching memories for {agent_id}...", spinner="dots"):
|
||||
data = make_api_request(
|
||||
method="POST",
|
||||
endpoint="/api/search",
|
||||
json_data={
|
||||
"query": query,
|
||||
"fact_type": list(fact_type),
|
||||
"agent_id": agent_id,
|
||||
"thinking_budget": thinking_budget,
|
||||
"max_tokens": max_tokens,
|
||||
"trace": trace,
|
||||
},
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
results = data.get("results", [])
|
||||
trace_data = data.get("trace")
|
||||
|
||||
# Display results
|
||||
if not results:
|
||||
console.print("[yellow]No results found.[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"\n[bold green]Found {len(results)} results:[/bold green]\n")
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
# Create a panel for each result
|
||||
score = result.get("score", 0.0)
|
||||
text = result.get("text", "")
|
||||
fact_type_val = result.get("fact_type", "unknown")
|
||||
context = result.get("context", "")
|
||||
date = result.get("date", "")
|
||||
|
||||
# Color code based on fact type
|
||||
type_colors = {
|
||||
"world": "cyan",
|
||||
"agent": "magenta",
|
||||
"opinion": "yellow"
|
||||
}
|
||||
color = type_colors.get(fact_type_val, "white")
|
||||
|
||||
# Build info line
|
||||
info_parts = [f"[{color}]{fact_type_val.upper()}[/{color}]"]
|
||||
if context:
|
||||
info_parts.append(f"Context: {context}")
|
||||
if date:
|
||||
info_parts.append(f"Date: {date}")
|
||||
info_parts.append(f"Score: {score:.3f}")
|
||||
|
||||
info_line = " | ".join(info_parts)
|
||||
|
||||
panel = Panel(
|
||||
f"{text}\n\n[dim]{info_line}[/dim]",
|
||||
title=f"[bold]Result {i}[/bold]",
|
||||
border_style=color,
|
||||
box=box.ROUNDED,
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
# Show trace if requested
|
||||
if trace and trace_data:
|
||||
console.print("\n[bold blue]Trace Information:[/bold blue]")
|
||||
|
||||
trace_table = Table(show_header=True, box=box.SIMPLE)
|
||||
trace_table.add_column("Metric", style="cyan")
|
||||
trace_table.add_column("Value", style="green")
|
||||
|
||||
if "search_time_seconds" in trace_data:
|
||||
trace_table.add_row("Search Time", f"{trace_data['search_time_seconds']:.3f}s")
|
||||
if "total_activated" in trace_data:
|
||||
trace_table.add_row("Total Activated", str(trace_data["total_activated"]))
|
||||
if "results_returned" in trace_data:
|
||||
trace_table.add_row("Results Returned", str(trace_data["results_returned"]))
|
||||
|
||||
console.print(trace_table)
|
||||
|
||||
|
||||
@app.command()
|
||||
def think(
|
||||
agent_id: str = typer.Argument(..., help="Agent ID"),
|
||||
query: str = typer.Argument(..., help="Question to think about"),
|
||||
thinking_budget: int = typer.Option(
|
||||
50, "--budget", "-b", help="Thinking budget"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Think and generate an answer using agent identity and memories.
|
||||
|
||||
Example:
|
||||
memora think alice "What do you think about machine learning?"
|
||||
"""
|
||||
with console.status(f"[bold blue]Thinking...", spinner="dots"):
|
||||
result = make_api_request(
|
||||
method="POST",
|
||||
endpoint="/api/think",
|
||||
json_data={
|
||||
"query": query,
|
||||
"agent_id": agent_id,
|
||||
"thinking_budget": thinking_budget,
|
||||
},
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
# Display answer
|
||||
console.print(Panel(
|
||||
Markdown(result["text"]),
|
||||
title=f"[bold cyan]Answer for {agent_id}[/bold cyan]",
|
||||
border_style="cyan",
|
||||
box=box.DOUBLE,
|
||||
))
|
||||
|
||||
# Display what the answer was based on
|
||||
based_on = result.get("based_on", {})
|
||||
if based_on:
|
||||
console.print("\n[bold blue]Based on:[/bold blue]\n")
|
||||
|
||||
for fact_type, facts in based_on.items():
|
||||
if facts:
|
||||
type_colors = {
|
||||
"world": "cyan",
|
||||
"agent": "magenta",
|
||||
"opinion": "yellow"
|
||||
}
|
||||
color = type_colors.get(fact_type, "white")
|
||||
|
||||
table = Table(
|
||||
title=f"[{color}]{fact_type.upper()}[/{color}]",
|
||||
show_header=True,
|
||||
box=box.ROUNDED,
|
||||
border_style=color,
|
||||
)
|
||||
table.add_column("Text", style="white", width=80)
|
||||
table.add_column("Score", justify="right", style="green", width=10)
|
||||
|
||||
for fact in facts[:5]: # Show top 5
|
||||
text = fact.get("text", "")
|
||||
score = fact.get("score", 0.0)
|
||||
table.add_row(text, f"{score:.3f}")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Display new opinions formed
|
||||
new_opinions = result.get("new_opinions", [])
|
||||
if new_opinions:
|
||||
console.print("\n[bold yellow]New Opinions Formed:[/bold yellow]\n")
|
||||
for opinion in new_opinions:
|
||||
console.print(Panel(
|
||||
f"{opinion['text']}\n\n[dim]Confidence: {opinion['confidence']:.2f}[/dim]",
|
||||
border_style="yellow",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
|
||||
|
||||
@app.command()
|
||||
def put(
|
||||
agent_id: str = typer.Argument(..., help="Agent ID"),
|
||||
content: str = typer.Argument(..., help="Memory content to store"),
|
||||
document_id: Optional[str] = typer.Option(
|
||||
None, "--doc-id", "-d", help="Document ID (auto-generated if not provided)"
|
||||
),
|
||||
context: Optional[str] = typer.Option(
|
||||
None, "--context", "-c", help="Context for the memory"
|
||||
),
|
||||
use_async: bool = typer.Option(
|
||||
False, "--async", help="Use async batch put (returns immediately, processes in background)"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Store a memory from text input.
|
||||
|
||||
Example:
|
||||
memora put alice "Alice loves machine learning and AI"
|
||||
memora put alice "Today we discussed neural networks" --context "team meeting"
|
||||
memora put alice "Important note" --async
|
||||
"""
|
||||
# Generate document_id if not provided
|
||||
if not document_id:
|
||||
document_id = f"cli_put_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
# Prepare content
|
||||
item = {"content": content}
|
||||
if context:
|
||||
item["context"] = context
|
||||
|
||||
# Choose endpoint based on async flag
|
||||
endpoint = "/api/memories/batch_async" if use_async else "/api/memories/batch"
|
||||
status_msg = "Queueing memory" if use_async else "Storing memory"
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task(f"[cyan]{status_msg} for {agent_id}...", total=None)
|
||||
|
||||
result = make_api_request(
|
||||
method="POST",
|
||||
endpoint=endpoint,
|
||||
json_data={
|
||||
"agent_id": agent_id,
|
||||
"items": [item],
|
||||
"document_id": document_id,
|
||||
},
|
||||
timeout=120.0,
|
||||
)
|
||||
progress.update(task, completed=True)
|
||||
|
||||
# Check if the result indicates success
|
||||
if not result.get("success", False):
|
||||
console.print(Panel(
|
||||
f"[red]✗[/red] Failed to store memory\n"
|
||||
f"[dim]Error:[/dim] {result.get('message', 'Unknown error')}",
|
||||
title="[bold red]Storage Failed[/bold red]",
|
||||
border_style="red",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Display result based on async vs sync
|
||||
if use_async and result.get("queued", False):
|
||||
console.print(Panel(
|
||||
f"[green]✓[/green] Memory queued for background processing\n"
|
||||
f"[dim]Agent ID:[/dim] {agent_id}\n"
|
||||
f"[dim]Document ID:[/dim] {document_id}\n"
|
||||
f"[dim]Content length:[/dim] {len(content)} characters\n"
|
||||
f"[dim]Items queued:[/dim] {result.get('items_count', 1)}\n"
|
||||
f"[yellow]Processing in background...[/yellow]",
|
||||
title="[bold green]Memory Queued[/bold green]",
|
||||
border_style="green",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
else:
|
||||
console.print(Panel(
|
||||
f"[green]✓[/green] Successfully stored memory\n"
|
||||
f"[dim]Agent ID:[/dim] {agent_id}\n"
|
||||
f"[dim]Document ID:[/dim] {document_id}\n"
|
||||
f"[dim]Content length:[/dim] {len(content)} characters\n"
|
||||
f"[dim]Items processed:[/dim] {result.get('items_count', 1)}",
|
||||
title="[bold green]Memory Stored[/bold green]",
|
||||
border_style="green",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
|
||||
|
||||
@app.command(name="put-files")
|
||||
def put_files(
|
||||
agent_id: str = typer.Argument(..., help="Agent ID"),
|
||||
path: str = typer.Argument(..., help="File or directory path"),
|
||||
recursive: bool = typer.Option(
|
||||
True, "--recursive/--no-recursive", "-r", help="Search directories recursively"
|
||||
),
|
||||
use_async: bool = typer.Option(
|
||||
False, "--async", help="Use async batch put (returns immediately, processes in background)"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Store memories from local files (.txt and .md only).
|
||||
Each file becomes a separate document with the filename as doc_id.
|
||||
|
||||
Example:
|
||||
memora put-files alice ./documents/
|
||||
memora put-files alice meeting-notes.txt
|
||||
memora put-files alice ./documents/ --async
|
||||
"""
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
console.print(f"[red]Error: Path '{path}' does not exist[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Collect files to process
|
||||
files_to_process = []
|
||||
|
||||
if path_obj.is_file():
|
||||
if path_obj.suffix.lower() in ['.txt', '.md']:
|
||||
files_to_process.append(path_obj)
|
||||
else:
|
||||
console.print(f"[yellow]Warning: Skipping '{path}' - only .txt and .md files are supported[/yellow]")
|
||||
raise typer.Exit(0)
|
||||
else:
|
||||
# Directory - find all .txt and .md files
|
||||
pattern = "**/*" if recursive else "*"
|
||||
for ext in ['.txt', '.md']:
|
||||
files_to_process.extend(path_obj.glob(f"{pattern}{ext}"))
|
||||
|
||||
if not files_to_process:
|
||||
console.print(f"[yellow]No .txt or .md files found in '{path}'[/yellow]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Display files to be processed
|
||||
console.print(f"\n[bold]Found {len(files_to_process)} files to process:[/bold]\n")
|
||||
|
||||
tree = Tree(f"[bold cyan]{path}[/bold cyan]")
|
||||
for file_path in sorted(files_to_process):
|
||||
size = file_path.stat().st_size
|
||||
size_str = f"{size:,} bytes" if size < 1024 else f"{size/1024:.1f} KB"
|
||||
tree.add(f"{file_path.name} [dim]({size_str})[/dim]")
|
||||
console.print(tree)
|
||||
console.print()
|
||||
|
||||
# Process files
|
||||
successful = 0
|
||||
failed = 0
|
||||
queued = 0
|
||||
|
||||
# Choose endpoint based on async flag
|
||||
endpoint = "/api/memories/batch_async" if use_async else "/api/memories/batch"
|
||||
status_msg = "Queueing files" if use_async else "Processing files"
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
console=console,
|
||||
) as progress:
|
||||
main_task = progress.add_task(
|
||||
f"[cyan]{status_msg} for {agent_id}...",
|
||||
total=len(files_to_process)
|
||||
)
|
||||
|
||||
for file_path in files_to_process:
|
||||
try:
|
||||
# Read file content
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
|
||||
# Use filename (without extension) as document_id
|
||||
doc_id = file_path.stem
|
||||
|
||||
# Prepare content
|
||||
item = {
|
||||
"content": content,
|
||||
"context": f"File: {file_path.name}"
|
||||
}
|
||||
|
||||
# Store memory via API
|
||||
result = make_api_request(
|
||||
method="POST",
|
||||
endpoint=endpoint,
|
||||
json_data={
|
||||
"agent_id": agent_id,
|
||||
"items": [item],
|
||||
"document_id": doc_id,
|
||||
},
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
# Check if the result indicates success
|
||||
if not result.get("success", False):
|
||||
raise Exception(result.get("message", "Unknown error"))
|
||||
|
||||
if use_async and result.get("queued", False):
|
||||
queued += 1
|
||||
else:
|
||||
successful += 1
|
||||
progress.update(main_task, advance=1)
|
||||
|
||||
except typer.Exit:
|
||||
# Re-raise typer.Exit to stop execution
|
||||
raise
|
||||
except Exception as e:
|
||||
console.print(f"[red]Failed to process {file_path.name}: {str(e)}[/red]")
|
||||
failed += 1
|
||||
progress.update(main_task, advance=1)
|
||||
|
||||
# Summary
|
||||
console.print()
|
||||
if use_async and queued > 0:
|
||||
console.print(Panel(
|
||||
f"[green]✓[/green] Successfully queued {queued} file(s) for background processing\n"
|
||||
f"[red]✗[/red] Failed: {failed}\n"
|
||||
f"[dim]Agent ID:[/dim] {agent_id}\n"
|
||||
f"[yellow]Processing in background...[/yellow]",
|
||||
title="[bold green]Files Queued[/bold green]",
|
||||
border_style="green" if failed == 0 else "yellow",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
elif successful > 0:
|
||||
console.print(Panel(
|
||||
f"[green]✓[/green] Successfully processed {successful} file(s)\n"
|
||||
f"[red]✗[/red] Failed: {failed}\n"
|
||||
f"[dim]Agent ID:[/dim] {agent_id}",
|
||||
title="[bold green]Files Processed[/bold green]",
|
||||
border_style="green" if failed == 0 else "yellow",
|
||||
box=box.ROUNDED,
|
||||
))
|
||||
else:
|
||||
console.print("[red]No files were successfully processed[/red]")
|
||||
|
||||
|
||||
@app.command()
|
||||
def agents():
|
||||
"""
|
||||
List all agents in the memory system.
|
||||
|
||||
Example:
|
||||
memora agents
|
||||
"""
|
||||
with console.status("[bold blue]Fetching agents...", spinner="dots"):
|
||||
data = make_api_request(
|
||||
method="GET",
|
||||
endpoint="/api/agents",
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
agent_list = data.get("agents", [])
|
||||
|
||||
if not agent_list:
|
||||
console.print("[yellow]No agents found in the system.[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"\n[bold green]Found {len(agent_list)} agent(s):[/bold green]\n")
|
||||
|
||||
table = Table(show_header=True, box=box.ROUNDED, border_style="cyan")
|
||||
table.add_column("#", style="dim", width=6)
|
||||
table.add_column("Agent ID", style="cyan")
|
||||
|
||||
for i, agent in enumerate(agent_list, 1):
|
||||
table.add_row(str(i), agent)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the CLI."""
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "memora-cli"
|
||||
version = "0.1.0"
|
||||
description = "Modern CLI for Memora - Temporal Semantic Memory System"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"rich>=13.0.0",
|
||||
"typer>=0.20.0",
|
||||
"httpx>=0.27.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
memora = "memora_cli.main:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["memora_cli"]
|
||||
279
memora-cli/src/api.rs
Normal file
279
memora-cli/src/api.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
use anyhow::{Context, Result};
|
||||
use reqwest::blocking::{Client, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct ApiError {
|
||||
pub url: String,
|
||||
pub request_body: String,
|
||||
pub response_status: Option<u16>,
|
||||
pub response_body: Option<String>,
|
||||
pub error: anyhow::Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SearchRequest {
|
||||
pub query: String,
|
||||
pub fact_type: Vec<String>,
|
||||
pub agent_id: String,
|
||||
pub thinking_budget: i32,
|
||||
pub max_tokens: i32,
|
||||
pub trace: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
pub results: Vec<Fact>,
|
||||
pub trace: Option<TraceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Fact {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
pub text: String,
|
||||
#[serde(rename = "type", default)]
|
||||
pub fact_type: Option<String>,
|
||||
pub activation: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub context: Option<String>,
|
||||
#[serde(default)]
|
||||
pub event_date: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TraceInfo {
|
||||
pub total_time: Option<f64>,
|
||||
pub activation_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ThinkRequest {
|
||||
pub query: String,
|
||||
pub agent_id: String,
|
||||
pub thinking_budget: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ThinkResponse {
|
||||
pub text: String,
|
||||
pub based_on: Vec<Fact>,
|
||||
pub new_opinions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MemoryItem {
|
||||
pub content: String,
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchMemoryRequest {
|
||||
pub agent_id: String,
|
||||
pub items: Vec<MemoryItem>,
|
||||
pub document_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BatchMemoryResponse {
|
||||
pub success: bool,
|
||||
pub stored_count: Option<i32>,
|
||||
pub error: Option<String>,
|
||||
pub job_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AgentsResponse {
|
||||
Success {
|
||||
agents: Vec<String>,
|
||||
},
|
||||
Error {
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Agent {
|
||||
pub agent_id: String,
|
||||
}
|
||||
|
||||
pub struct ApiClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(base_url: String) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
Ok(ApiClient { client, base_url })
|
||||
}
|
||||
|
||||
pub fn search(&self, request: SearchRequest, verbose: bool) -> Result<SearchResponse> {
|
||||
let url = format!("{}/api/search", self.base_url);
|
||||
let request_body = serde_json::to_string_pretty(&request).unwrap_or_default();
|
||||
|
||||
if verbose {
|
||||
eprintln!("Request URL: {}", url);
|
||||
eprintln!("Request body:\n{}", request_body);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&request)
|
||||
.timeout(Duration::from_secs(120))
|
||||
.send()?;
|
||||
|
||||
let status = response.status();
|
||||
if verbose {
|
||||
eprintln!("Response status: {}", status);
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response.text().unwrap_or_default();
|
||||
if verbose {
|
||||
eprintln!("Error response body:\n{}", error_body);
|
||||
}
|
||||
anyhow::bail!("API returned error status {}: {}", status, error_body);
|
||||
}
|
||||
|
||||
let response_text = response.text()?;
|
||||
if verbose {
|
||||
eprintln!("Response body:\n{}", response_text);
|
||||
}
|
||||
|
||||
let result: SearchResponse = serde_json::from_str(&response_text)
|
||||
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn think(&self, request: ThinkRequest, verbose: bool) -> Result<ThinkResponse> {
|
||||
let url = format!("{}/api/think", self.base_url);
|
||||
|
||||
if verbose {
|
||||
eprintln!("Request URL: {}", url);
|
||||
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&request)
|
||||
.timeout(Duration::from_secs(120))
|
||||
.send()?;
|
||||
|
||||
let status = response.status();
|
||||
if verbose {
|
||||
eprintln!("Response status: {}", status);
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response.text().unwrap_or_default();
|
||||
if verbose {
|
||||
eprintln!("Error response body:\n{}", error_body);
|
||||
}
|
||||
anyhow::bail!("API returned error status {}: {}", status, error_body);
|
||||
}
|
||||
|
||||
let response_text = response.text()?;
|
||||
if verbose {
|
||||
eprintln!("Response body:\n{}", response_text);
|
||||
}
|
||||
|
||||
let result: ThinkResponse = serde_json::from_str(&response_text)
|
||||
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn put_memories(&self, request: BatchMemoryRequest, async_mode: bool, verbose: bool) -> Result<BatchMemoryResponse> {
|
||||
let endpoint = if async_mode {
|
||||
"batch_async"
|
||||
} else {
|
||||
"batch"
|
||||
};
|
||||
let url = format!("{}/api/memories/{}", self.base_url, endpoint);
|
||||
|
||||
if verbose {
|
||||
eprintln!("Request URL: {}", url);
|
||||
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&request)
|
||||
.timeout(Duration::from_secs(120))
|
||||
.send()?;
|
||||
|
||||
let status = response.status();
|
||||
if verbose {
|
||||
eprintln!("Response status: {}", status);
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response.text().unwrap_or_default();
|
||||
if verbose {
|
||||
eprintln!("Error response body:\n{}", error_body);
|
||||
}
|
||||
anyhow::bail!("API returned error status {}: {}", status, error_body);
|
||||
}
|
||||
|
||||
let response_text = response.text()?;
|
||||
if verbose {
|
||||
eprintln!("Response body:\n{}", response_text);
|
||||
}
|
||||
|
||||
let result: BatchMemoryResponse = serde_json::from_str(&response_text)
|
||||
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn list_agents(&self, verbose: bool) -> Result<Vec<Agent>> {
|
||||
let url = format!("{}/api/agents", self.base_url);
|
||||
|
||||
if verbose {
|
||||
eprintln!("Request URL: {}", url);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.send()?;
|
||||
|
||||
let status = response.status();
|
||||
if verbose {
|
||||
eprintln!("Response status: {}", status);
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let error_body = response.text().unwrap_or_default();
|
||||
if verbose {
|
||||
eprintln!("Error response body:\n{}", error_body);
|
||||
}
|
||||
anyhow::bail!("API returned error status {}: {}", status, error_body);
|
||||
}
|
||||
|
||||
let response_text = response.text()?;
|
||||
if verbose {
|
||||
eprintln!("Response body:\n{}", response_text);
|
||||
}
|
||||
|
||||
let result: AgentsResponse = serde_json::from_str(&response_text)
|
||||
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
|
||||
|
||||
match result {
|
||||
AgentsResponse::Success { agents } => {
|
||||
Ok(agents.into_iter().map(|agent_id| Agent { agent_id }).collect())
|
||||
}
|
||||
AgentsResponse::Error { error } => {
|
||||
anyhow::bail!("Failed to list agents: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
memora-cli/src/config.rs
Normal file
32
memora-cli/src/config.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
|
||||
pub struct Config {
|
||||
pub api_url: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let api_url = env::var("MEMORA_API_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||
|
||||
// Validate URL format
|
||||
if !api_url.starts_with("http://") && !api_url.starts_with("https://") {
|
||||
anyhow::bail!(
|
||||
"Invalid API URL: {}. Must start with http:// or https://",
|
||||
api_url
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Config { api_url })
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &str {
|
||||
&self.api_url
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_doc_id() -> String {
|
||||
let now = chrono::Local::now();
|
||||
format!("cli_put_{}", now.format("%Y%m%d_%H%M%S"))
|
||||
}
|
||||
180
memora-cli/src/errors.rs
Normal file
180
memora-cli/src/errors.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
use anyhow::Result;
|
||||
use colored::*;
|
||||
|
||||
pub fn handle_api_error(err: anyhow::Error, api_url: &str) -> ! {
|
||||
eprintln!("{}", format_error_message(&err, api_url));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn format_error_message(err: &anyhow::Error, api_url: &str) -> String {
|
||||
let err_str = err.to_string();
|
||||
|
||||
// Connection refused
|
||||
if err_str.contains("Connection refused") || err_str.contains("tcp connect error") || err_str.contains("error sending request") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}\n\n{}\n {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Cannot connect to Memora API".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"The Memora API server is not running".bright_white(),
|
||||
format!("The server is running on a different address than {}", api_url).bright_white(),
|
||||
"A firewall is blocking the connection".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Start the Memora API server and ensure it's accessible".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if err_str.contains("timeout") || err_str.contains("Timeout") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n • {}\n • {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Request timed out".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"The API server is slow to respond".bright_white(),
|
||||
"Network latency is too high".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Check if the API server is healthy".bright_white(),
|
||||
"Try again with a better network connection".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// DNS/Host resolution
|
||||
if err_str.contains("dns") || err_str.contains("DNS") || err_str.contains("failed to lookup") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Cannot resolve API hostname".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"The hostname in the API URL is incorrect".bright_white(),
|
||||
"DNS server is not responding".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Check the MEMORA_API_URL environment variable".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
if err_str.contains("404") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
||||
"✗".bright_red().bold(),
|
||||
"API endpoint not found (404)".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"The API endpoint path has changed".bright_white(),
|
||||
"You're using an incompatible API version".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Check that you're using the correct Memora API version".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// 401/403 Authentication
|
||||
if err_str.contains("401") || err_str.contains("403") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Authentication failed".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"API requires authentication".bright_white(),
|
||||
"Invalid or missing credentials".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Check if the API requires an API key or token".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// 500 Server Error
|
||||
if err_str.contains("500") || err_str.contains("502") || err_str.contains("503") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n\n{}\n • {}\n • {}",
|
||||
"✗".bright_red().bold(),
|
||||
"API server error".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"The server encountered an error:".bright_yellow(),
|
||||
"Internal server error (500)".bright_white(),
|
||||
"Service temporarily unavailable".bright_white(),
|
||||
"Try:".bright_green(),
|
||||
"Check the API server logs for details".bright_white(),
|
||||
"Try again in a few moments".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// Invalid URL
|
||||
if err_str.contains("invalid URL") || err_str.contains("InvalidUri") {
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n {}\n\n{}\n {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Invalid API URL".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"The API URL format is invalid.".bright_yellow(),
|
||||
"Ensure it starts with http:// or https://".bright_white(),
|
||||
"Example:".bright_green(),
|
||||
"export MEMORA_API_URL=http://localhost:8080".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// JSON parsing error - show actual response
|
||||
if err_str.contains("Failed to parse") || err_str.contains("error decoding") {
|
||||
// Extract the actual response if available
|
||||
let response_hint = if err_str.contains("Response was:") {
|
||||
let parts: Vec<&str> = err_str.split("Response was:").collect();
|
||||
if parts.len() > 1 {
|
||||
format!("\n{}\n{}", "Actual response:".bright_yellow(), parts[1].trim().bright_white())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
return format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}{}\n\n{}\n • {}\n • {}",
|
||||
"✗".bright_red().bold(),
|
||||
"Invalid API response format".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Possible causes:".bright_yellow(),
|
||||
"The API returned an unexpected response format".bright_white(),
|
||||
"Version mismatch between CLI and API".bright_white(),
|
||||
"The API endpoint doesn't exist or returned HTML instead of JSON".bright_white(),
|
||||
response_hint,
|
||||
"Try:".bright_green(),
|
||||
"Run with --verbose flag to see the full request/response".bright_white(),
|
||||
"Ensure you're using a compatible Memora API version".bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// Generic error with the full error message
|
||||
format!(
|
||||
"{} {}\n\n{}\n {}\n\n{}\n {}\n\n{}\n • {}\n • {}\n • {}",
|
||||
"✗".bright_red().bold(),
|
||||
"API request failed".bright_red().bold(),
|
||||
"API URL:".bright_yellow(),
|
||||
api_url.bright_white(),
|
||||
"Error:".bright_yellow(),
|
||||
err_str.bright_white(),
|
||||
"Suggestions:".bright_green(),
|
||||
"Check that MEMORA_API_URL is set correctly".bright_white(),
|
||||
"Ensure the Memora API server is running".bright_white(),
|
||||
"Verify network connectivity to the API server".bright_white()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print_config_help() {
|
||||
println!("\n{}", "Configuration:".bright_cyan().bold());
|
||||
println!(" Set the API URL using an environment variable:");
|
||||
println!(" {}", "export MEMORA_API_URL=http://localhost:8080".bright_white());
|
||||
println!("\n Add to your shell profile to make it permanent:");
|
||||
println!(" {}", "echo 'export MEMORA_API_URL=http://localhost:8080' >> ~/.zshrc".bright_black());
|
||||
println!();
|
||||
}
|
||||
439
memora-cli/src/main.rs
Normal file
439
memora-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
mod api;
|
||||
mod config;
|
||||
mod errors;
|
||||
mod output;
|
||||
mod ui;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use api::{ApiClient, BatchMemoryRequest, MemoryItem, SearchRequest, ThinkRequest};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use config::Config;
|
||||
use output::OutputFormat;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum Format {
|
||||
Pretty,
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
impl From<Format> for OutputFormat {
|
||||
fn from(f: Format) -> Self {
|
||||
match f {
|
||||
Format::Pretty => OutputFormat::Pretty,
|
||||
Format::Json => OutputFormat::Json,
|
||||
Format::Yaml => OutputFormat::Yaml,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "memora")]
|
||||
#[command(about = "Memora CLI - Semantic memory system", long_about = None)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
/// Output format (pretty, json, yaml)
|
||||
#[arg(short = 'o', long, global = true, default_value = "pretty")]
|
||||
output: Format,
|
||||
|
||||
/// Show verbose output including full requests and responses
|
||||
#[arg(short = 'v', long, global = true)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Search for memories using semantic search
|
||||
Search {
|
||||
/// Agent ID to search for
|
||||
agent_id: String,
|
||||
|
||||
/// Search query
|
||||
query: String,
|
||||
|
||||
/// Fact types to search (world, agent, opinion)
|
||||
#[arg(short = 't', long, value_delimiter = ',', default_values = &["world", "agent", "opinion"])]
|
||||
fact_type: Vec<String>,
|
||||
|
||||
/// Thinking budget for search
|
||||
#[arg(short = 'b', long, default_value = "100")]
|
||||
budget: i32,
|
||||
|
||||
/// Maximum tokens for results
|
||||
#[arg(long, default_value = "4096")]
|
||||
max_tokens: i32,
|
||||
|
||||
/// Show trace information (timing, activation counts)
|
||||
#[arg(long)]
|
||||
trace: bool,
|
||||
},
|
||||
|
||||
/// Generate answers using agent identity and memories
|
||||
Think {
|
||||
/// Agent ID to think as
|
||||
agent_id: String,
|
||||
|
||||
/// Query to think about
|
||||
query: String,
|
||||
|
||||
/// Thinking budget
|
||||
#[arg(short = 'b', long, default_value = "50")]
|
||||
budget: i32,
|
||||
},
|
||||
|
||||
/// Store a single memory
|
||||
Put {
|
||||
/// Agent ID to store memory for
|
||||
agent_id: String,
|
||||
|
||||
/// Memory content to store
|
||||
content: String,
|
||||
|
||||
/// Document ID (auto-generated if not provided)
|
||||
#[arg(short = 'd', long)]
|
||||
doc_id: Option<String>,
|
||||
|
||||
/// Context for the memory
|
||||
#[arg(short = 'c', long)]
|
||||
context: Option<String>,
|
||||
|
||||
/// Queue for background processing
|
||||
#[arg(long)]
|
||||
r#async: bool,
|
||||
},
|
||||
|
||||
/// Bulk import memories from files
|
||||
PutFiles {
|
||||
/// Agent ID to store memories for
|
||||
agent_id: String,
|
||||
|
||||
/// Path to file or directory
|
||||
path: PathBuf,
|
||||
|
||||
/// Search directories recursively
|
||||
#[arg(short = 'r', long, default_value = "true")]
|
||||
recursive: bool,
|
||||
|
||||
/// Queue for background processing
|
||||
#[arg(long)]
|
||||
r#async: bool,
|
||||
},
|
||||
|
||||
/// List all agents
|
||||
Agents,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let output_format: OutputFormat = cli.output.into();
|
||||
let verbose = cli.verbose;
|
||||
|
||||
// Load configuration
|
||||
let config = Config::from_env().unwrap_or_else(|e| {
|
||||
ui::print_error(&format!("Configuration error: {}", e));
|
||||
errors::print_config_help();
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let api_url = config.api_url().to_string();
|
||||
|
||||
// Create API client
|
||||
let client = ApiClient::new(api_url.clone()).unwrap_or_else(|e| {
|
||||
errors::handle_api_error(e, &api_url);
|
||||
});
|
||||
|
||||
// Execute command and handle errors
|
||||
let result: Result<()> = match cli.command {
|
||||
Commands::Search {
|
||||
agent_id,
|
||||
query,
|
||||
fact_type,
|
||||
budget,
|
||||
max_tokens,
|
||||
trace,
|
||||
} => {
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Searching memories..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let request = SearchRequest {
|
||||
query,
|
||||
fact_type,
|
||||
agent_id,
|
||||
thinking_budget: budget,
|
||||
max_tokens,
|
||||
trace,
|
||||
};
|
||||
|
||||
let response = client.search(request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_search_results(&resp, trace);
|
||||
} else {
|
||||
output::print_output(&resp, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Think {
|
||||
agent_id,
|
||||
query,
|
||||
budget,
|
||||
} => {
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Thinking..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let request = ThinkRequest {
|
||||
query,
|
||||
agent_id,
|
||||
thinking_budget: budget,
|
||||
};
|
||||
|
||||
let response = client.think(request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_think_response(&resp);
|
||||
} else {
|
||||
output::print_output(&resp, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Put {
|
||||
agent_id,
|
||||
content,
|
||||
doc_id,
|
||||
context,
|
||||
r#async,
|
||||
} => {
|
||||
let doc_id = doc_id.unwrap_or_else(config::generate_doc_id);
|
||||
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Storing memory..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let item = MemoryItem {
|
||||
content: content.clone(),
|
||||
context,
|
||||
};
|
||||
|
||||
let request = BatchMemoryRequest {
|
||||
agent_id,
|
||||
items: vec![item],
|
||||
document_id: Some(doc_id.clone()),
|
||||
};
|
||||
|
||||
let response = client.put_memories(request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_stored_memory(&doc_id, &content, r#async);
|
||||
if let Some(job_id) = resp.job_id {
|
||||
ui::print_info(&format!("Job ID: {}", job_id));
|
||||
}
|
||||
} else {
|
||||
output::print_output(&resp, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
Commands::PutFiles {
|
||||
agent_id,
|
||||
path,
|
||||
recursive,
|
||||
r#async,
|
||||
} => {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("Path does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
if path.is_file() {
|
||||
files.push(path);
|
||||
} else if path.is_dir() {
|
||||
if recursive {
|
||||
for entry in WalkDir::new(&path)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let path = entry.path();
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "txt" || ext == "md" {
|
||||
files.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "txt" || ext == "md" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if files.is_empty() {
|
||||
ui::print_warning("No .txt or .md files found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ui::print_info(&format!("Found {} files to import", files.len()));
|
||||
|
||||
let pb = ui::create_progress_bar(files.len() as u64, "Processing files");
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut document_id = None;
|
||||
|
||||
for file_path in &files {
|
||||
let content = fs::read_to_string(file_path)
|
||||
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
|
||||
|
||||
let doc_id = file_path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(config::generate_doc_id);
|
||||
|
||||
// Use the first file's stem as the document_id for the batch
|
||||
if document_id.is_none() {
|
||||
document_id = Some(doc_id);
|
||||
}
|
||||
|
||||
items.push(MemoryItem {
|
||||
content,
|
||||
context: None,
|
||||
});
|
||||
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
pb.finish_with_message("Files processed");
|
||||
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Uploading memories..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let request = BatchMemoryRequest {
|
||||
agent_id,
|
||||
items,
|
||||
document_id,
|
||||
};
|
||||
let response = client.put_memories(request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
if r#async {
|
||||
ui::print_success(&format!(
|
||||
"Queued {} files for background processing",
|
||||
files.len()
|
||||
));
|
||||
if let Some(job_id) = resp.job_id {
|
||||
ui::print_info(&format!("Job ID: {}", job_id));
|
||||
}
|
||||
} else {
|
||||
ui::print_success(&format!("Successfully stored {} memories", files.len()));
|
||||
}
|
||||
} else {
|
||||
output::print_output(&resp, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Agents => {
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Fetching agents..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = client.list_agents(verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(agents) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_agents_table(&agents);
|
||||
} else {
|
||||
output::print_output(&agents, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle API errors with nice messages
|
||||
if let Err(e) = result {
|
||||
errors::handle_api_error(e, &api_url);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
36
memora-cli/src/output.rs
Normal file
36
memora-cli/src/output.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum OutputFormat {
|
||||
Pretty,
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"pretty" | "default" => Some(OutputFormat::Pretty),
|
||||
"json" => Some(OutputFormat::Json),
|
||||
"yaml" | "yml" => Some(OutputFormat::Yaml),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<()> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(data)?);
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", serde_yaml::to_string(data)?);
|
||||
}
|
||||
OutputFormat::Pretty => {
|
||||
// This should not be called - pretty printing is handled in ui.rs
|
||||
unreachable!("Pretty format should be handled separately")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
217
memora-cli/src/ui.rs
Normal file
217
memora-cli/src/ui.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
use crate::api::{Agent, Fact, SearchResponse, ThinkResponse, TraceInfo};
|
||||
use colored::*;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub fn print_banner() {
|
||||
println!("{}", "╔══════════════════════════════════════════════════╗".bright_cyan());
|
||||
println!("{}", "║ MEMORA - Memory CLI ║".bright_cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════╝".bright_cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_section_header(title: &str) {
|
||||
println!();
|
||||
println!("{}", format!("━━━ {} ━━━", title).bright_yellow().bold());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_fact(fact: &Fact, show_activation: bool) {
|
||||
let fact_type = fact.fact_type.as_deref().unwrap_or("unknown");
|
||||
|
||||
let type_color = match fact_type {
|
||||
"world" => "cyan",
|
||||
"agent" => "magenta",
|
||||
"opinion" => "yellow",
|
||||
_ => "white",
|
||||
};
|
||||
|
||||
let prefix = match fact_type {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "📝",
|
||||
};
|
||||
|
||||
print!("{} ", prefix);
|
||||
print!("{}", format!("[{}]", fact_type.to_uppercase()).color(type_color).bold());
|
||||
|
||||
if show_activation {
|
||||
if let Some(activation) = fact.activation {
|
||||
print!(" {}", format!("({:.2})", activation).bright_black());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(" {}", fact.text);
|
||||
|
||||
// Show context if available
|
||||
if let Some(context) = &fact.context {
|
||||
println!(" {}: {}", "Context".bright_black(), context.bright_black());
|
||||
}
|
||||
|
||||
// Show event date if available
|
||||
if let Some(event_date) = &fact.event_date {
|
||||
println!(" {}: {}", "Date".bright_black(), event_date.bright_black());
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_search_results(response: &SearchResponse, show_trace: bool) {
|
||||
let results = &response.results;
|
||||
print_section_header(&format!("Search Results ({})", results.len()));
|
||||
|
||||
if results.is_empty() {
|
||||
println!("{}", " No results found.".bright_black());
|
||||
} else {
|
||||
for (i, fact) in results.iter().enumerate() {
|
||||
println!("{}", format!(" Result #{}", i + 1).bright_black());
|
||||
print_fact(fact, true);
|
||||
}
|
||||
}
|
||||
|
||||
if show_trace {
|
||||
if let Some(trace) = &response.trace {
|
||||
print_trace_info(trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_think_response(response: &ThinkResponse) {
|
||||
print_section_header("Answer");
|
||||
println!("{}", response.text.bright_white());
|
||||
println!();
|
||||
|
||||
// Note: based_on facts are hidden in default output
|
||||
// Use -o json to see the complete response including based_on facts
|
||||
if !response.based_on.is_empty() {
|
||||
println!(" {}", format!("(Based on {} facts - use -o json to see details)", response.based_on.len()).bright_black());
|
||||
println!();
|
||||
}
|
||||
|
||||
if !response.new_opinions.is_empty() {
|
||||
print_section_header(&format!("New opinions formed ({})", response.new_opinions.len()));
|
||||
for opinion in &response.new_opinions {
|
||||
println!(" 💭 {}", opinion.bright_yellow());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_trace_info(trace: &TraceInfo) {
|
||||
print_section_header("Trace Information");
|
||||
|
||||
if let Some(time) = trace.total_time {
|
||||
println!(" ⏱️ Total time: {}", format!("{:.2}ms", time).bright_green());
|
||||
}
|
||||
|
||||
if let Some(count) = trace.activation_count {
|
||||
println!(" 📊 Activation count: {}", count.to_string().bright_green());
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_agents_table(agents: &[Agent]) {
|
||||
print_section_header(&format!("Agents ({})", agents.len()));
|
||||
|
||||
if agents.is_empty() {
|
||||
println!("{}", " No agents found.".bright_black());
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate column width
|
||||
let max_id_len = agents.iter().map(|a| a.agent_id.len()).max().unwrap_or(8);
|
||||
let id_width = max_id_len.max(8);
|
||||
|
||||
// Print header
|
||||
println!(" ┌{}┐",
|
||||
"─".repeat(id_width + 2));
|
||||
|
||||
println!(" │ {:<width$} │",
|
||||
"Agent ID".bright_cyan().bold(),
|
||||
width = id_width);
|
||||
|
||||
println!(" ├{}┤",
|
||||
"─".repeat(id_width + 2));
|
||||
|
||||
// Print rows
|
||||
for agent in agents {
|
||||
println!(" │ {:<width$} │",
|
||||
agent.agent_id,
|
||||
width = id_width);
|
||||
}
|
||||
|
||||
println!(" └{}┘",
|
||||
"─".repeat(id_width + 2));
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_success(message: &str) {
|
||||
println!("{} {}", "✓".bright_green().bold(), message.bright_white());
|
||||
}
|
||||
|
||||
pub fn print_error(message: &str) {
|
||||
eprintln!("{} {}", "✗".bright_red().bold(), message.bright_red());
|
||||
}
|
||||
|
||||
pub fn print_warning(message: &str) {
|
||||
println!("{} {}", "⚠".bright_yellow().bold(), message.bright_yellow());
|
||||
}
|
||||
|
||||
pub fn print_info(message: &str) {
|
||||
println!("{} {}", "ℹ".bright_blue().bold(), message.bright_white());
|
||||
}
|
||||
|
||||
pub fn create_spinner(message: &str) -> ProgressBar {
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.cyan} {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
||||
);
|
||||
pb.set_message(message.to_string());
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
pb
|
||||
}
|
||||
|
||||
pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||
let pb = ProgressBar::new(total);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{msg} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%)")
|
||||
.unwrap()
|
||||
.progress_chars("█▓▒░ "),
|
||||
);
|
||||
pb.set_message(message.to_string());
|
||||
pb
|
||||
}
|
||||
|
||||
pub fn print_stored_memory(doc_id: &str, content: &str, is_async: bool) {
|
||||
if is_async {
|
||||
println!("{} Queued for background processing", "⏳".bright_yellow());
|
||||
} else {
|
||||
println!("{} Stored successfully", "✓".bright_green());
|
||||
}
|
||||
println!(" Document ID: {}", doc_id.bright_cyan());
|
||||
let preview = if content.len() > 60 {
|
||||
format!("{}...", &content[..57])
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
println!(" Content: {}", preview.bright_black());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
||||
print!("{} {} [y/N]: ", "?".bright_blue().bold(), message);
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@ node_modules
|
|||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.env.local
|
||||
.env*.local
|
||||
.env
|
||||
README.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
2
memora-control-plane/.env.example
Normal file
2
memora-control-plane/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Dataplane API URL
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
39
memora-control-plane/Dockerfile
Normal file
39
memora-control-plane/Dockerfile
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
|
@ -75,16 +75,12 @@ npm install
|
|||
|
||||
### Configuration
|
||||
|
||||
Configure the dataplane URL in `.env.local`:
|
||||
Configure the dataplane URL in `.env` (optional, defaults to http://localhost:8080):
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local`:
|
||||
|
||||
```env
|
||||
DATAPLANE_API_URL=http://localhost:8080
|
||||
cat > .env << 'EOF'
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
EOF
|
||||
```
|
||||
|
||||
### Development
|
||||
|
|
@ -157,7 +153,7 @@ control-plane/
|
|||
│ ├── agent-context.tsx # Global agent state
|
||||
│ ├── api.ts # API client
|
||||
│ └── utils.ts # Utilities
|
||||
├── .env.local # Environment config
|
||||
├── .env # Environment config (optional)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
|
|
@ -240,7 +236,7 @@ All control plane API routes proxy to the dataplane:
|
|||
|
||||
**CORS Errors**: The control plane should eliminate CORS issues. If you see them, ensure you're accessing the control plane at `http://localhost:3000` (not the dataplane directly).
|
||||
|
||||
**Connection Errors**: Verify the dataplane is running at the URL specified in `.env.local` (default: `http://localhost:8080`).
|
||||
**Connection Errors**: Verify the dataplane is running at the URL specified in `.env` (default: `http://localhost:8080`).
|
||||
|
||||
**Graph Not Rendering**: Check browser console for errors. Ensure data is loading correctly from `/api/graph`.
|
||||
|
||||
|
|
@ -62,11 +62,11 @@ echo "Image: ${FULL_IMAGE_NAME}"
|
|||
echo ""
|
||||
echo "To run the container:"
|
||||
echo " docker run -p 3000:3000 \\"
|
||||
echo " -e DATAPLANE_API_URL=http://your-api-url:8080 \\"
|
||||
echo " -e MEMORA_CP_DATAPLANE_API_URL=http://your-api-url:8080 \\"
|
||||
echo " ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
echo "Or with an env file:"
|
||||
echo " docker run -p 3000:3000 --env-file .env.local ${FULL_IMAGE_NAME}"
|
||||
echo " docker run -p 3000:3000 --env-file .env ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
echo "To push to registry (if registry specified):"
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
const DATAPLANE_URL = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -213,7 +213,7 @@ export class ServerDataplaneClient {
|
|||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
this.baseUrl = process.env.MEMORA_CP_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
}
|
||||
|
||||
async fetchDataplane<T>(
|
||||
10
memora-control-plane/start-server.sh
Executable file
10
memora-control-plane/start-server.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
# Wrapper script to map MEMORA_CP_* environment variables to Next.js standard variables
|
||||
|
||||
# Map prefixed env vars to standard Next.js env vars
|
||||
export HOSTNAME="${MEMORA_CP_HOSTNAME:-0.0.0.0}"
|
||||
export PORT="${MEMORA_CP_PORT:-3000}"
|
||||
|
||||
# Start the Next.js server
|
||||
# The server.js is in the standalone output at the root
|
||||
exec node server.js
|
||||
Binary file not shown.
|
|
@ -230,11 +230,11 @@ class BenchmarkRunner:
|
|||
self.answer_generator = answer_generator
|
||||
self.answer_evaluator = answer_evaluator
|
||||
self.memory = memory or TemporalSemanticMemory(
|
||||
db_url=os.getenv("DATABASE_URL"),
|
||||
memory_llm_provider=os.getenv("MEMORY_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("MEMORY_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("MEMORY_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||
memory_llm_base_url=os.getenv("MEMORY_LLM_BASE_URL") or None, # Use None to get provider defaults
|
||||
db_url=os.getenv("MEMORA_API_DATABASE_URL"),
|
||||
memory_llm_provider=os.getenv("MEMORA_API_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("MEMORA_API_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("MEMORA_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||
memory_llm_base_url=os.getenv("MEMORA_API_LLM_BASE_URL") or None, # Use None to get provider defaults
|
||||
)
|
||||
|
||||
def calculate_data_stats(self, items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
|
|
@ -350,11 +350,11 @@ async def run_benchmark(
|
|||
memory = RemoteMemoryClient(base_url=api_url)
|
||||
else:
|
||||
memory = TemporalSemanticMemory(
|
||||
db_url=os.getenv("DATABASE_URL"),
|
||||
memory_llm_provider=os.getenv("MEMORY_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("MEMORY_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("MEMORY_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||
memory_llm_base_url=os.getenv("MEMORY_LLM_BASE_URL") or None, # Use None to get provider defaults
|
||||
db_url=os.getenv("MEMORA_API_DATABASE_URL"),
|
||||
memory_llm_provider=os.getenv("MEMORA_API_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("MEMORA_API_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("MEMORA_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||
memory_llm_base_url=os.getenv("MEMORA_API_LLM_BASE_URL") or None, # Use None to get provider defaults
|
||||
)
|
||||
await memory.initialize()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue