standalone and fix
This commit is contained in:
parent
646dea89d9
commit
a1c5f9847a
25 changed files with 1509 additions and 184 deletions
BIN
benchmarks/.DS_Store
vendored
BIN
benchmarks/.DS_Store
vendored
Binary file not shown.
|
|
@ -62,7 +62,7 @@ export function DocumentsView() {
|
|||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search documents (ID, metadata)..."
|
||||
placeholder="Search documents (ID)..."
|
||||
className="w-full max-w-2xl px-2.5 py-2 mb-4 mx-5 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
|
||||
|
|
@ -75,7 +75,6 @@ export function DocumentsView() {
|
|||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Updated</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Text Length</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Memory Units</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Metadata</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -94,11 +93,6 @@ export function DocumentsView() {
|
|||
</td>
|
||||
<td className="p-2 border border-border">{doc.text_length?.toLocaleString()} chars</td>
|
||||
<td className="p-2 border border-border">{doc.memory_unit_count}</td>
|
||||
<td className="p-2 border border-border" title={JSON.stringify(doc.metadata)}>
|
||||
{Object.keys(doc.metadata || {}).length > 0
|
||||
? JSON.stringify(doc.metadata).substring(0, 50) + '...'
|
||||
: 'None'}
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
<button
|
||||
onClick={() => viewDocumentText(doc.id)}
|
||||
|
|
@ -112,7 +106,7 @@ export function DocumentsView() {
|
|||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-10 text-center text-muted-foreground bg-muted">
|
||||
<td colSpan={6} className="p-10 text-center text-muted-foreground bg-muted">
|
||||
Click "Load Documents" to view data
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
79
memora-cli/README.md
Normal file
79
memora-cli/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# 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
|
||||
5
memora-cli/memora_cli/__init__.py
Normal file
5
memora-cli/memora_cli/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
Memora CLI - Modern command-line interface for the Memora Memory System.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
565
memora-cli/memora_cli/main.py
Normal file
565
memora-cli/memora_cli/main.py
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
"""
|
||||
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()
|
||||
21
memora-cli/pyproject.toml
Normal file
21
memora-cli/pyproject.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[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"]
|
||||
|
|
@ -21,7 +21,7 @@ from memora import TemporalSemanticMemory
|
|||
class SearchRequest(BaseModel):
|
||||
"""Request model for search endpoint."""
|
||||
query: str
|
||||
fact_type: List[str] # List of fact types to search
|
||||
fact_type: Optional[List[str]] = None # List of fact types to search (defaults to all if not specified)
|
||||
agent_id: str = "default"
|
||||
thinking_budget: int = 100
|
||||
max_tokens: int = 4096
|
||||
|
|
@ -274,7 +274,6 @@ class ListDocumentsResponse(BaseModel):
|
|||
"id": "session_1",
|
||||
"agent_id": "user123",
|
||||
"content_hash": "abc123",
|
||||
"metadata": {"source": "conversation"},
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
"text_length": 5420,
|
||||
|
|
@ -294,7 +293,6 @@ class DocumentResponse(BaseModel):
|
|||
agent_id: str
|
||||
original_text: str
|
||||
content_hash: Optional[str]
|
||||
metadata: Dict[str, Any]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
memory_unit_count: int
|
||||
|
|
@ -306,7 +304,6 @@ class DocumentResponse(BaseModel):
|
|||
"agent_id": "user123",
|
||||
"original_text": "Full document text here...",
|
||||
"content_hash": "abc123",
|
||||
"metadata": {"source": "conversation"},
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
"memory_unit_count": 15
|
||||
|
|
@ -380,15 +377,6 @@ The system uses:
|
|||
def _register_routes(app: FastAPI):
|
||||
"""Register all API routes on the given app instance."""
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def index():
|
||||
"""Root endpoint - directs to control plane."""
|
||||
return {
|
||||
"message": "Memory Control Plane API",
|
||||
"docs": "/docs",
|
||||
"control_plane": "The web UI has moved to the Next.js control plane. Please use the control-plane directory."
|
||||
}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/api/graph",
|
||||
|
|
@ -472,12 +460,10 @@ def _register_routes(app: FastAPI):
|
|||
# Validate fact_type(s)
|
||||
valid_fact_types = ["world", "agent", "opinion"]
|
||||
|
||||
# Default to all fact types if not specified
|
||||
if not request.fact_type:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="fact_type must be a non-empty list"
|
||||
)
|
||||
|
||||
request.fact_type = valid_fact_types
|
||||
else:
|
||||
for ft in request.fact_type:
|
||||
if ft not in valid_fact_types:
|
||||
raise HTTPException(
|
||||
|
|
@ -508,11 +494,22 @@ def _register_routes(app: FastAPI):
|
|||
question_date=question_date
|
||||
)
|
||||
|
||||
# Filter results to only include specific fields
|
||||
filtered_results = [
|
||||
{
|
||||
"id": result.get("id"),
|
||||
"text": result.get("text"),
|
||||
"context": result.get("context"),
|
||||
"event_date": result.get("event_date")
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
|
||||
# Convert trace to dict
|
||||
trace_dict = trace.to_dict() if trace else None
|
||||
|
||||
return SearchResponse(
|
||||
results=results,
|
||||
results=filtered_results,
|
||||
trace=trace_dict
|
||||
)
|
||||
except HTTPException:
|
||||
|
|
@ -672,7 +669,7 @@ def _register_routes(app: FastAPI):
|
|||
description="List documents with pagination and optional search. Documents are the source content from which memory units are extracted."
|
||||
)
|
||||
async def api_list_documents(
|
||||
agent_id: Optional[str] = None,
|
||||
agent_id: str,
|
||||
q: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0
|
||||
|
|
@ -681,7 +678,7 @@ def _register_routes(app: FastAPI):
|
|||
List documents for an agent with optional search.
|
||||
|
||||
Args:
|
||||
agent_id: Filter by agent ID
|
||||
agent_id: Agent ID (required)
|
||||
q: Search query (searches document ID and metadata)
|
||||
limit: Maximum number of results (default: 100)
|
||||
offset: Offset for pagination (default: 0)
|
||||
|
|
@ -760,14 +757,6 @@ def _register_routes(app: FastAPI):
|
|||
)
|
||||
async def api_batch_put(request: BatchPutRequest):
|
||||
try:
|
||||
# Validate agent_id - prevent writing to reserved agents
|
||||
RESERVED_AGENT_IDS = {"locomo"}
|
||||
if request.agent_id in RESERVED_AGENT_IDS:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Cannot write to reserved agent_id '{request.agent_id}'. Reserved agents: {', '.join(RESERVED_AGENT_IDS)}"
|
||||
)
|
||||
|
||||
# Prepare contents for put_batch_async
|
||||
contents = []
|
||||
for item in request.items:
|
||||
|
|
@ -784,7 +773,7 @@ def _register_routes(app: FastAPI):
|
|||
contents=contents,
|
||||
document_id=request.document_id
|
||||
)
|
||||
logging.info(f"Batch put result: {result}")
|
||||
|
||||
|
||||
return BatchPutResponse(
|
||||
success=True,
|
||||
|
|
@ -830,14 +819,6 @@ def _register_routes(app: FastAPI):
|
|||
)
|
||||
async def api_batch_put_async(request: BatchPutRequest):
|
||||
try:
|
||||
# Validate agent_id - prevent writing to reserved agents
|
||||
RESERVED_AGENT_IDS = {"locomo"}
|
||||
if request.agent_id in RESERVED_AGENT_IDS:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Cannot write to reserved agent_id '{request.agent_id}'. Reserved agents: {', '.join(RESERVED_AGENT_IDS)}"
|
||||
)
|
||||
|
||||
# Prepare contents for put_batch_async
|
||||
contents = []
|
||||
for item in request.items:
|
||||
|
|
|
|||
|
|
@ -57,11 +57,15 @@ class ThinkOperationsMixin:
|
|||
fact_type=['agent', 'world', 'opinion']
|
||||
)
|
||||
|
||||
logger.info(f"[THINK] Search returned {len(all_results)} results")
|
||||
|
||||
# Split results by fact type for structured response
|
||||
agent_results = [r for r in all_results if r.get('fact_type') == 'agent']
|
||||
world_results = [r for r in all_results if r.get('fact_type') == 'world']
|
||||
opinion_results = [r for r in all_results if r.get('fact_type') == 'opinion']
|
||||
|
||||
logger.info(f"[THINK] Split results - agent: {len(agent_results)}, world: {len(world_results)}, opinion: {len(opinion_results)}")
|
||||
|
||||
# Step 4: Format facts for LLM with full details as JSON
|
||||
import json
|
||||
|
||||
|
|
@ -99,6 +103,8 @@ class ThinkOperationsMixin:
|
|||
world_facts_text = format_facts(world_results)
|
||||
opinion_facts_text = format_facts(opinion_results)
|
||||
|
||||
logger.info(f"[THINK] Formatted facts - agent: {len(agent_facts_text)} chars, world: {len(world_facts_text)} chars, opinion: {len(opinion_facts_text)} chars")
|
||||
|
||||
# Step 5: Call Groq to formulate answer
|
||||
prompt = f"""You are an AI assistant answering a question based on retrieved facts provided in JSON format.
|
||||
|
||||
|
|
@ -123,6 +129,9 @@ Provide a helpful, accurate answer based on the facts above. Be consistent with
|
|||
|
||||
If you form any new opinions while thinking about this question, state them clearly in your answer."""
|
||||
|
||||
logger.info(f"[THINK] Full prompt length: {len(prompt)} chars")
|
||||
logger.debug(f"[THINK] Prompt preview (first 500 chars): {prompt[:500]}")
|
||||
|
||||
answer_text = await self._llm_config.call(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful AI assistant. Always respond in plain text without markdown formatting. You can form and express opinions based on facts."},
|
||||
|
|
|
|||
|
|
@ -1281,6 +1281,7 @@ class TemporalSemanticMemory(
|
|||
"text": data["text"],
|
||||
"context": data.get("context", ""),
|
||||
"event_date": data["event_date"], # Keep as datetime for now
|
||||
"fact_type": data.get("fact_type"), # Include fact type for filtering
|
||||
"access_count": data.get("access_count", 0),
|
||||
"semantic_similarity": semantic_sim,
|
||||
"bm25_score": bm25_score,
|
||||
|
|
@ -1447,12 +1448,12 @@ class TemporalSemanticMemory(
|
|||
async with pool.acquire() as conn:
|
||||
doc = await conn.fetchrow(
|
||||
"""
|
||||
SELECT d.id, d.agent_id, d.original_text, d.content_hash, d.metadata,
|
||||
SELECT d.id, d.agent_id, d.original_text, d.content_hash,
|
||||
d.created_at, d.updated_at, COUNT(mu.id) as unit_count
|
||||
FROM documents d
|
||||
LEFT JOIN memory_units mu ON mu.document_id = d.id
|
||||
WHERE d.id = $1 AND d.agent_id = $2
|
||||
GROUP BY d.id, d.agent_id, d.original_text, d.content_hash, d.metadata, d.created_at, d.updated_at
|
||||
GROUP BY d.id, d.agent_id, d.original_text, d.content_hash, d.created_at, d.updated_at
|
||||
""",
|
||||
document_id, agent_id
|
||||
)
|
||||
|
|
@ -1460,13 +1461,11 @@ class TemporalSemanticMemory(
|
|||
if not doc:
|
||||
return None
|
||||
|
||||
import json
|
||||
return {
|
||||
"id": doc["id"],
|
||||
"agent_id": doc["agent_id"],
|
||||
"original_text": doc["original_text"],
|
||||
"content_hash": doc["content_hash"],
|
||||
"metadata": json.loads(doc["metadata"]) if doc["metadata"] else {},
|
||||
"unit_count": doc["unit_count"],
|
||||
"created_at": doc["created_at"],
|
||||
"updated_at": doc["updated_at"]
|
||||
|
|
@ -1871,7 +1870,7 @@ class TemporalSemanticMemory(
|
|||
|
||||
async def list_documents(
|
||||
self,
|
||||
agent_id: Optional[str] = None,
|
||||
agent_id: str,
|
||||
search_query: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0
|
||||
|
|
@ -1880,8 +1879,8 @@ class TemporalSemanticMemory(
|
|||
List documents with optional search and pagination.
|
||||
|
||||
Args:
|
||||
agent_id: Filter by agent ID
|
||||
search_query: Search in metadata (JSON text search)
|
||||
agent_id: Agent ID (required)
|
||||
search_query: Search in document ID
|
||||
limit: Maximum number of results
|
||||
offset: Offset for pagination
|
||||
|
||||
|
|
@ -1895,15 +1894,14 @@ class TemporalSemanticMemory(
|
|||
query_params = []
|
||||
param_count = 0
|
||||
|
||||
if agent_id:
|
||||
param_count += 1
|
||||
query_conditions.append(f"agent_id = ${param_count}")
|
||||
query_params.append(agent_id)
|
||||
|
||||
if search_query:
|
||||
# Search in document ID and metadata (as text)
|
||||
# Search in document ID
|
||||
param_count += 1
|
||||
query_conditions.append(f"(id ILIKE ${param_count} OR metadata::text ILIKE ${param_count})")
|
||||
query_conditions.append(f"id ILIKE ${param_count}")
|
||||
query_params.append(f"%{search_query}%")
|
||||
|
||||
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
|
||||
|
|
@ -1931,7 +1929,6 @@ class TemporalSemanticMemory(
|
|||
id,
|
||||
agent_id,
|
||||
content_hash,
|
||||
metadata,
|
||||
created_at,
|
||||
updated_at,
|
||||
LENGTH(original_text) as text_length
|
||||
|
|
@ -1979,7 +1976,6 @@ class TemporalSemanticMemory(
|
|||
"id": doc_id,
|
||||
"agent_id": agent_id_val,
|
||||
"content_hash": row['content_hash'],
|
||||
"metadata": row['metadata'] if row['metadata'] else {},
|
||||
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
|
||||
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
|
||||
"text_length": row['text_length'] or 0,
|
||||
|
|
@ -2016,7 +2012,6 @@ class TemporalSemanticMemory(
|
|||
agent_id,
|
||||
original_text,
|
||||
content_hash,
|
||||
metadata,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM documents
|
||||
|
|
@ -2038,7 +2033,6 @@ class TemporalSemanticMemory(
|
|||
"agent_id": doc['agent_id'],
|
||||
"original_text": doc['original_text'],
|
||||
"content_hash": doc['content_hash'],
|
||||
"metadata": doc['metadata'] if doc['metadata'] else {},
|
||||
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
|
||||
"updated_at": doc['updated_at'].isoformat() if doc['updated_at'] else "",
|
||||
"memory_unit_count": unit_count_row['unit_count'] if unit_count_row else 0
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
"""
|
||||
Performance tuning test using real LoComo conversation.
|
||||
|
||||
This test loads a long conversation (419 dialogues across 19 sessions),
|
||||
ingests it into memory, and runs searches to measure performance.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Configure logging to show performance metrics
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s:%(name)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(300) # 5 minute timeout for performance test
|
||||
async def test_batch_ingestion_single_call(memory):
|
||||
"""
|
||||
Test ingesting entire conversation in ONE batch call.
|
||||
|
||||
This is the most efficient way - all sessions in one put_batch_async.
|
||||
"""
|
||||
# Load conversation fixture
|
||||
fixture_path = Path(__file__).parent / "fixtures" / "locomo_conversation_sample.json"
|
||||
with open(fixture_path) as f:
|
||||
conversation_data = json.load(f)
|
||||
|
||||
sample_id = conversation_data['sample_id']
|
||||
logging.info(f"\n{'='*80}")
|
||||
logging.info(f"BATCH INGESTION TEST: {sample_id}")
|
||||
logging.info(f"{'='*80}")
|
||||
|
||||
agent_id = f"batch_test_{sample_id}_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
# Parse all sessions into batch format
|
||||
# LIMIT to first 5 sessions for faster iteration during perf tuning
|
||||
MAX_SESSIONS = 5
|
||||
logging.info(f"\nPreparing batch contents (limiting to {MAX_SESSIONS} sessions for perf tuning)...")
|
||||
conversation = conversation_data['conversation']
|
||||
batch_contents = []
|
||||
|
||||
for i in range(1, MAX_SESSIONS + 1):
|
||||
session_key = f'session_{i}'
|
||||
session_date_key = f'session_{i}_date_time'
|
||||
|
||||
if session_key not in conversation or not conversation[session_key]:
|
||||
break
|
||||
|
||||
session_dialogues = conversation[session_key]
|
||||
session_date = conversation.get(session_date_key, datetime.now(timezone.utc).isoformat())
|
||||
|
||||
# Combine dialogues
|
||||
session_text = "\n".join([
|
||||
f"{d['speaker']}: {d['text']}"
|
||||
for d in session_dialogues
|
||||
])
|
||||
|
||||
# Parse date
|
||||
try:
|
||||
from dateutil import parser as date_parser
|
||||
event_date = date_parser.isoparse(session_date)
|
||||
except:
|
||||
event_date = datetime.now(timezone.utc)
|
||||
|
||||
batch_contents.append({
|
||||
'content': session_text,
|
||||
'context': f'session_{i}',
|
||||
'event_date': event_date
|
||||
})
|
||||
|
||||
logging.info(f"Prepared {len(batch_contents)} sessions for batch ingestion")
|
||||
|
||||
# Single batch call
|
||||
logging.info(f"\nIngesting all {len(batch_contents)} sessions in ONE batch call...")
|
||||
result_ids = await memory.put_batch_async(
|
||||
agent_id=agent_id,
|
||||
contents=batch_contents,
|
||||
document_id=f"{agent_id}_full_conversation"
|
||||
)
|
||||
|
||||
total_units = sum(len(ids) for ids in result_ids)
|
||||
logging.info(f"\n{'='*80}")
|
||||
logging.info(f"BATCH INGESTION COMPLETE: {total_units} memory units created")
|
||||
logging.info(f"{'='*80}")
|
||||
|
||||
# Run one sample search
|
||||
logging.info(f"\nRunning sample search...")
|
||||
question = conversation_data['qa'][0]['question']
|
||||
logging.info(f"Question: {question}")
|
||||
|
||||
results, _ = await memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query=question,
|
||||
fact_type=["world"],
|
||||
thinking_budget=100,
|
||||
top_k=5,
|
||||
enable_trace=False
|
||||
)
|
||||
|
||||
logging.info(f"Found {len(results)} results")
|
||||
if results:
|
||||
logging.info(f"Top result: {results[0]['text'][:100]}...")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
logging.info("\nCleaning up...")
|
||||
await memory.delete_agent(agent_id)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[tool.uv.workspace]
|
||||
members = ["memora", "benchmarks", "memora-dev"]
|
||||
members = ["memora", "benchmarks", "memora-dev", "memora-cli"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = []
|
||||
|
|
|
|||
56
scripts/start-control-plane.sh
Executable file
56
scripts/start-control-plane.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../control-plane"
|
||||
|
||||
# Parse arguments
|
||||
PORT=3000
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--port|-p)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --port, -p PORT Port to run on (default: 3000)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --port 3001"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if .env.local exists
|
||||
if [ ! -f ".env.local" ]; then
|
||||
echo "⚠️ Warning: .env.local not found"
|
||||
echo "Creating from .env.local.example..."
|
||||
if [ -f ".env.local.example" ]; then
|
||||
cp .env.local.example .env.local
|
||||
echo "✅ Created .env.local"
|
||||
echo "📝 Please edit .env.local if you need to change the DATAPLANE_API_URL"
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Error: .env.local.example not found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Control Plane (Next.js dev server)..."
|
||||
echo "📄 Loading environment from .env.local"
|
||||
echo ""
|
||||
echo "Control plane will be available at: http://localhost:${PORT}"
|
||||
echo ""
|
||||
|
||||
# Set the port and run dev server
|
||||
PORT=$PORT npm run dev
|
||||
46
standalone/.dockerignore
Normal file
46
standalone/.dockerignore
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Build artifacts
|
||||
**/*.pyc
|
||||
**/__pycache__/
|
||||
**/.pytest_cache/
|
||||
**/.venv/
|
||||
**/venv/
|
||||
**/*.egg-info/
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# Node
|
||||
**/node_modules/
|
||||
**/.next/
|
||||
**/npm-debug.log
|
||||
**/.turbo/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!standalone/.env.standalone
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Test and dev files
|
||||
**/tests/
|
||||
local-db/
|
||||
logs/
|
||||
|
||||
# Documentation (except standalone README)
|
||||
README.md
|
||||
!standalone/README.md
|
||||
|
||||
# Standalone files
|
||||
standalone/build-docker.sh
|
||||
standalone/.dockerignore
|
||||
standalone/.env.example
|
||||
standalone/docker-compose.yml
|
||||
9
standalone/.env.example
Normal file
9
standalone/.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Environment variables for docker-compose
|
||||
# Copy this file to .env and customize as needed
|
||||
|
||||
# Optional: OpenAI API key
|
||||
# OPENAI_API_KEY=your-api-key-here
|
||||
|
||||
# Optional: Custom embedding model
|
||||
# EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
||||
# EMBEDDING_DIM=384
|
||||
15
standalone/.env.standalone
Normal file
15
standalone/.env.standalone
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Standalone environment configuration
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/memora
|
||||
DATAPLANE_API_URL=http://localhost:8080
|
||||
|
||||
# Embedding configuration
|
||||
EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
||||
EMBEDDING_DIM=384
|
||||
|
||||
# LLM Provider (set to "none" to disable LLM features)
|
||||
LLM_PROVIDER=none
|
||||
|
||||
# Optional: LLM API Keys
|
||||
# OPENAI_API_KEY=your-openai-key-here
|
||||
# ANTHROPIC_API_KEY=your-anthropic-key-here
|
||||
# GROQ_API_KEY=your-groq-key-here
|
||||
5
standalone/.gitignore
vendored
Normal file
5
standalone/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Environment files
|
||||
.env
|
||||
|
||||
# Docker volumes
|
||||
*.log
|
||||
87
standalone/Dockerfile
Normal file
87
standalone/Dockerfile
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
FROM node:20-alpine AS control-plane-builder
|
||||
|
||||
# Build control plane
|
||||
WORKDIR /app/control-plane
|
||||
COPY control-plane/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY control-plane/ ./
|
||||
# Set env to skip font optimization during build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build || (echo "Build failed, retrying..." && npm run build)
|
||||
|
||||
# Python source stage - just copy files, don't build venv yet
|
||||
FROM python:3.12-slim AS dataplane-source
|
||||
|
||||
WORKDIR /build
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY memora/ ./memora/
|
||||
COPY benchmarks/ ./benchmarks/
|
||||
COPY memora-dev/ ./memora-dev/
|
||||
COPY memora-cli/ ./memora-cli/
|
||||
|
||||
# Final runtime image
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install system dependencies and PostgreSQL
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
wget \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& mkdir -p /etc/apt/keyrings \
|
||||
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/pgdg.gpg \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||
&& apt-get update && apt-get install -y \
|
||||
postgresql-15 \
|
||||
postgresql-15-pgvector \
|
||||
postgresql-contrib-15 \
|
||||
nodejs \
|
||||
npm \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv
|
||||
RUN pip install uv
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dataplane source from builder
|
||||
COPY --from=dataplane-source /build /app
|
||||
|
||||
# Build venv in the final stage to ensure compatibility
|
||||
RUN cd /app && uv sync --frozen
|
||||
|
||||
# Copy control plane from builder
|
||||
COPY --from=control-plane-builder /app/control-plane/.next/standalone /app/control-plane
|
||||
COPY --from=control-plane-builder /app/control-plane/.next/static /app/control-plane/.next/static
|
||||
|
||||
# Copy standalone configuration
|
||||
COPY standalone/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY standalone/init.sh /app/init.sh
|
||||
COPY standalone/.env.standalone /app/.env
|
||||
|
||||
RUN chmod +x /app/init.sh
|
||||
|
||||
# PostgreSQL setup
|
||||
RUN mkdir -p /var/lib/postgresql/data && \
|
||||
chown -R postgres:postgres /var/lib/postgresql && \
|
||||
mkdir -p /var/run/postgresql && \
|
||||
chown -R postgres:postgres /var/run/postgresql
|
||||
|
||||
# Initialize PostgreSQL as postgres user
|
||||
USER postgres
|
||||
RUN /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/data
|
||||
|
||||
USER root
|
||||
|
||||
# Expose ports
|
||||
# 5432: PostgreSQL
|
||||
# 8080: Dataplane API
|
||||
# 3000: Control Plane
|
||||
EXPOSE 5432 8080 3000
|
||||
|
||||
# Start supervisor
|
||||
CMD ["/app/init.sh"]
|
||||
104
standalone/QUICKSTART.md
Normal file
104
standalone/QUICKSTART.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Memora Standalone - Quick Start
|
||||
|
||||
## What is this?
|
||||
|
||||
A single Docker image containing everything you need to run Memora:
|
||||
- ✅ PostgreSQL database
|
||||
- ✅ Dataplane API (FastAPI backend)
|
||||
- ✅ Control Plane (Next.js web UI)
|
||||
|
||||
## Fastest Start (Docker Compose)
|
||||
|
||||
```bash
|
||||
cd standalone
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Access the UI at: **http://localhost:3000**
|
||||
|
||||
## Manual Docker Build & Run
|
||||
|
||||
### Build the image:
|
||||
```bash
|
||||
./standalone/build-docker.sh
|
||||
```
|
||||
|
||||
### Run with the helper script:
|
||||
```bash
|
||||
./standalone/run-docker.sh --persist
|
||||
```
|
||||
|
||||
### Or run directly:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name memora \
|
||||
-p 3000:3000 \
|
||||
-p 8080:8080 \
|
||||
-p 5432:5432 \
|
||||
-v memora-data:/var/lib/postgresql/data \
|
||||
memora-standalone:latest
|
||||
```
|
||||
|
||||
## Access Points
|
||||
|
||||
| Service | URL | Purpose |
|
||||
|---------|-----|---------|
|
||||
| **Control Plane** | http://localhost:3000 | Web UI |
|
||||
| **Dataplane API** | http://localhost:8080 | REST API |
|
||||
| **PostgreSQL** | localhost:5432 | Database |
|
||||
|
||||
## View Logs
|
||||
|
||||
```bash
|
||||
docker logs -f memora-standalone
|
||||
```
|
||||
|
||||
## Stop & Remove
|
||||
|
||||
```bash
|
||||
# Stop
|
||||
docker-compose down
|
||||
|
||||
# Stop and remove data
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set in `docker-compose.yml` or pass with `-e`:
|
||||
|
||||
- `EMBEDDING_MODEL_NAME` - Sentence transformer model (default: sentence-transformers/all-MiniLM-L6-v2)
|
||||
- `EMBEDDING_DIM` - Embedding dimension (default: 384)
|
||||
- `OPENAI_API_KEY` - Optional OpenAI API key
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Container won't start:**
|
||||
```bash
|
||||
docker logs memora-standalone
|
||||
```
|
||||
|
||||
**Database issues:**
|
||||
```bash
|
||||
docker exec -it memora-standalone su - postgres -c "psql memora"
|
||||
```
|
||||
|
||||
**Reset everything:**
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Production Notes
|
||||
|
||||
This standalone image is ideal for:
|
||||
- ✅ Development
|
||||
- ✅ Demos
|
||||
- ✅ Testing
|
||||
- ✅ Small deployments
|
||||
|
||||
For production, consider:
|
||||
- Separate containers for each service
|
||||
- External PostgreSQL database
|
||||
- Kubernetes/Docker Swarm orchestration
|
||||
- Environment-specific configurations
|
||||
124
standalone/README.md
Normal file
124
standalone/README.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Memora Standalone Docker Image
|
||||
|
||||
This directory contains the configuration to build a standalone Docker image that includes all Memora components in a single container:
|
||||
|
||||
- **PostgreSQL**: Database backend
|
||||
- **Dataplane**: FastAPI backend service
|
||||
- **Control Plane**: Next.js web interface
|
||||
|
||||
## Quick Start with Docker Compose
|
||||
|
||||
The easiest way to run the standalone image:
|
||||
|
||||
```bash
|
||||
cd standalone
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This will build and start all services with persistent data storage.
|
||||
|
||||
To stop:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
To remove data and start fresh:
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Building Manually
|
||||
|
||||
```bash
|
||||
./standalone/build-docker.sh
|
||||
```
|
||||
|
||||
With custom options:
|
||||
```bash
|
||||
./standalone/build-docker.sh --name my-memora --tag v1.0.0
|
||||
./standalone/build-docker.sh --registry docker.io/myuser --tag latest
|
||||
```
|
||||
|
||||
## Running Manually
|
||||
|
||||
Using the run script (recommended):
|
||||
```bash
|
||||
./standalone/run-docker.sh --persist
|
||||
```
|
||||
|
||||
With custom ports:
|
||||
```bash
|
||||
./standalone/run-docker.sh --persist --port-control 3001 --port-api 8081
|
||||
```
|
||||
|
||||
Direct docker run:
|
||||
```bash
|
||||
docker run -p 3000:3000 -p 8080:8080 memora-standalone:latest
|
||||
```
|
||||
|
||||
With persistent data:
|
||||
```bash
|
||||
docker run -p 3000:3000 -p 8080:8080 \
|
||||
-v memora-data:/var/lib/postgresql/data \
|
||||
memora-standalone:latest
|
||||
```
|
||||
|
||||
With custom environment variables:
|
||||
```bash
|
||||
docker run -p 3000:3000 -p 8080:8080 \
|
||||
-e OPENAI_API_KEY=your-key \
|
||||
-e EMBEDDING_MODEL_NAME=custom-model \
|
||||
memora-standalone:latest
|
||||
```
|
||||
|
||||
## Accessing Services
|
||||
|
||||
Once running, services are available at:
|
||||
|
||||
- **Control Plane**: http://localhost:3000
|
||||
- **Dataplane API**: http://localhost:8080
|
||||
- **PostgreSQL**: localhost:5432 (username: postgres, password: postgres, database: memora)
|
||||
|
||||
## Architecture
|
||||
|
||||
The container uses `supervisord` to manage three processes:
|
||||
1. PostgreSQL (started first)
|
||||
2. Dataplane API (started after PostgreSQL)
|
||||
3. Control Plane (started after dataplane)
|
||||
|
||||
The `init.sh` script handles:
|
||||
- PostgreSQL initialization
|
||||
- Database creation
|
||||
- Running migrations
|
||||
- Starting all services via supervisord
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/memora` | PostgreSQL connection string |
|
||||
| `DATAPLANE_API_URL` | `http://localhost:8080` | Dataplane API URL for control plane |
|
||||
| `EMBEDDING_MODEL_NAME` | `sentence-transformers/all-MiniLM-L6-v2` | Sentence transformer model |
|
||||
| `EMBEDDING_DIM` | `384` | Embedding dimension |
|
||||
| `OPENAI_API_KEY` | - | Optional OpenAI API key |
|
||||
|
||||
## Logs
|
||||
|
||||
View logs from all services:
|
||||
```bash
|
||||
docker logs -f <container-id>
|
||||
```
|
||||
|
||||
## Production Considerations
|
||||
|
||||
This standalone image is designed for:
|
||||
- Development environments
|
||||
- Demos and testing
|
||||
- Small deployments
|
||||
|
||||
For production use, consider:
|
||||
- Using separate containers for each service
|
||||
- External PostgreSQL database
|
||||
- Load balancing for the control plane
|
||||
- Persistent volume for PostgreSQL data
|
||||
- Environment-specific configurations
|
||||
87
standalone/build-docker.sh
Executable file
87
standalone/build-docker.sh
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Default values
|
||||
IMAGE_NAME="memora-standalone"
|
||||
IMAGE_TAG="latest"
|
||||
REGISTRY=""
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--name)
|
||||
IMAGE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--tag)
|
||||
IMAGE_TAG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--registry)
|
||||
REGISTRY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --name NAME Docker image name (default: memora-standalone)"
|
||||
echo " --tag TAG Docker image tag (default: latest)"
|
||||
echo " --registry REG Docker registry URL (optional)"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --name myapp --tag v1.0.0"
|
||||
echo " $0 --registry docker.io/myuser --name memora-standalone --tag v1.0.0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Construct full image name
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
else
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
fi
|
||||
|
||||
echo "Building Memora Standalone Docker image: ${FULL_IMAGE_NAME}"
|
||||
echo "============================================================="
|
||||
echo "This image includes:"
|
||||
echo " - PostgreSQL database"
|
||||
echo " - Dataplane API (FastAPI)"
|
||||
echo " - Control Plane (Next.js)"
|
||||
echo ""
|
||||
|
||||
# Build the Docker image
|
||||
docker build -f standalone/Dockerfile -t "${FULL_IMAGE_NAME}" .
|
||||
|
||||
echo ""
|
||||
echo "Build completed successfully!"
|
||||
echo "Image: ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
echo "To run the container:"
|
||||
echo " docker run -p 3000:3000 -p 8080:8080 ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
echo "Services will be available at:"
|
||||
echo " - Control Plane: http://localhost:3000"
|
||||
echo " - Dataplane API: http://localhost:8080"
|
||||
echo " - PostgreSQL: localhost:5432"
|
||||
echo ""
|
||||
echo "For persistent data, mount a volume:"
|
||||
echo " docker run -p 3000:3000 -p 8080:8080 \\"
|
||||
echo " -v memora-data:/var/lib/postgresql/data \\"
|
||||
echo " ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
echo "To push to registry:"
|
||||
echo " docker push ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
fi
|
||||
21
standalone/docker-compose.yml
Normal file
21
standalone/docker-compose.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
memora-standalone:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: standalone/Dockerfile
|
||||
ports:
|
||||
- "3000:3000" # Control Plane
|
||||
- "8080:8080" # Dataplane API
|
||||
- "5432:5432" # PostgreSQL
|
||||
environment:
|
||||
- EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
||||
- EMBEDDING_DIM=384
|
||||
# - OPENAI_API_KEY=${OPENAI_API_KEY} # Uncomment if needed
|
||||
volumes:
|
||||
- memora-data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
memora-data:
|
||||
55
standalone/init.sh
Executable file
55
standalone/init.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Memora Standalone Container..."
|
||||
echo "==========================================="
|
||||
|
||||
# Start PostgreSQL temporarily for initialization
|
||||
echo "📦 Starting PostgreSQL for initialization..."
|
||||
su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data -l /tmp/postgresql-init.log start"
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "⏳ Waiting for PostgreSQL to be ready..."
|
||||
for i in {1..30}; do
|
||||
if su - postgres -c "psql -lqt" &>/dev/null; then
|
||||
echo "✅ PostgreSQL is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "❌ PostgreSQL failed to start"
|
||||
cat /tmp/postgresql-init.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Create database if it doesn't exist
|
||||
echo "📊 Setting up database..."
|
||||
su - postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname = 'memora'\" | grep -q 1 || psql -c 'CREATE DATABASE memora;'"
|
||||
|
||||
# Run migrations
|
||||
echo "🔄 Running database migrations..."
|
||||
cd /app/memora
|
||||
|
||||
# Export environment variables
|
||||
set -a
|
||||
source /app/.env
|
||||
set +a
|
||||
|
||||
/app/.venv/bin/python -m alembic upgrade head
|
||||
|
||||
# Stop PostgreSQL so supervisord can start it cleanly
|
||||
echo "🔄 Stopping PostgreSQL to hand off to supervisord..."
|
||||
su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data stop -m fast"
|
||||
sleep 2
|
||||
|
||||
echo "✅ Initialization complete"
|
||||
echo ""
|
||||
echo "Starting services via supervisord..."
|
||||
echo " - PostgreSQL: localhost:5432"
|
||||
echo " - Dataplane API: http://localhost:8080"
|
||||
echo " - Control Plane: http://localhost:3000"
|
||||
echo ""
|
||||
|
||||
# Start supervisor to manage all services
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
122
standalone/run-docker.sh
Executable file
122
standalone/run-docker.sh
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
IMAGE_NAME="memora-standalone:latest"
|
||||
CONTAINER_NAME="memora-standalone"
|
||||
PERSIST_DATA=false
|
||||
PORT_CONTROL=3000
|
||||
PORT_API=8080
|
||||
PORT_DB=5432
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--image)
|
||||
IMAGE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--name)
|
||||
CONTAINER_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--persist)
|
||||
PERSIST_DATA=true
|
||||
shift
|
||||
;;
|
||||
--port-control)
|
||||
PORT_CONTROL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--port-api)
|
||||
PORT_API="$2"
|
||||
shift 2
|
||||
;;
|
||||
--port-db)
|
||||
PORT_DB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --image NAME Docker image name (default: memora-standalone:latest)"
|
||||
echo " --name NAME Container name (default: memora-standalone)"
|
||||
echo " --persist Use persistent volume for data"
|
||||
echo " --port-control PORT Control plane port (default: 3000)"
|
||||
echo " --port-api PORT Dataplane API port (default: 8080)"
|
||||
echo " --port-db PORT PostgreSQL port (default: 5432)"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --persist --port-control 3001"
|
||||
echo ""
|
||||
echo "To stop the container:"
|
||||
echo " docker stop ${CONTAINER_NAME}"
|
||||
echo ""
|
||||
echo "To remove the container:"
|
||||
echo " docker rm ${CONTAINER_NAME}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if container already exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "⚠️ Container '${CONTAINER_NAME}' already exists"
|
||||
echo ""
|
||||
read -p "Do you want to remove it and create a new one? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🗑️ Removing existing container..."
|
||||
docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
||||
else
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Memora Standalone Container"
|
||||
echo "========================================"
|
||||
echo "Image: ${IMAGE_NAME}"
|
||||
echo "Container: ${CONTAINER_NAME}"
|
||||
echo ""
|
||||
|
||||
# Build docker run command
|
||||
DOCKER_CMD="docker run -d --name ${CONTAINER_NAME}"
|
||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_CONTROL}:3000"
|
||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_API}:8080"
|
||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_DB}:5432"
|
||||
|
||||
if [ "$PERSIST_DATA" = true ]; then
|
||||
DOCKER_CMD="${DOCKER_CMD} -v memora-data:/var/lib/postgresql/data"
|
||||
echo "📦 Using persistent volume: memora-data"
|
||||
fi
|
||||
|
||||
DOCKER_CMD="${DOCKER_CMD} ${IMAGE_NAME}"
|
||||
|
||||
# Run the container
|
||||
eval $DOCKER_CMD
|
||||
|
||||
echo ""
|
||||
echo "✅ Container started successfully!"
|
||||
echo ""
|
||||
echo "Services are available at:"
|
||||
echo " - Control Plane: http://localhost:${PORT_CONTROL}"
|
||||
echo " - Dataplane API: http://localhost:${PORT_API}"
|
||||
echo " - PostgreSQL: localhost:${PORT_DB}"
|
||||
echo ""
|
||||
echo "View logs:"
|
||||
echo " docker logs -f ${CONTAINER_NAME}"
|
||||
echo ""
|
||||
echo "Stop container:"
|
||||
echo " docker stop ${CONTAINER_NAME}"
|
||||
echo ""
|
||||
echo "Remove container:"
|
||||
echo " docker rm -f ${CONTAINER_NAME}"
|
||||
echo ""
|
||||
42
standalone/supervisord.conf
Normal file
42
standalone/supervisord.conf
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:postgresql]
|
||||
command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data
|
||||
user=postgres
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=1
|
||||
|
||||
[program:dataplane]
|
||||
command=/app/.venv/bin/python -m memora.web.server --host 0.0.0.0 --port 8080
|
||||
directory=/app/memora
|
||||
environment=PATH="/app/.venv/bin:%(ENV_PATH)s",DATABASE_URL="postgresql://postgres:postgres@localhost:5432/memora",EMBEDDING_MODEL_NAME="sentence-transformers/all-MiniLM-L6-v2",EMBEDDING_DIM="384"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=10
|
||||
priority=10
|
||||
|
||||
[program:control-plane]
|
||||
command=/usr/bin/node /app/control-plane/server.js
|
||||
directory=/app/control-plane
|
||||
environment=NODE_ENV="production",PORT="3000",HOSTNAME="0.0.0.0",DATAPLANE_API_URL="http://localhost:8080"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startsecs=5
|
||||
priority=20
|
||||
18
uv.lock
18
uv.lock
|
|
@ -10,6 +10,7 @@ resolution-markers = [
|
|||
members = [
|
||||
"benchmarks",
|
||||
"memora",
|
||||
"memora-cli",
|
||||
"memora-dev",
|
||||
]
|
||||
|
||||
|
|
@ -1093,6 +1094,23 @@ requires-dist = [
|
|||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memora-cli"
|
||||
version = "0.1.0"
|
||||
source = { editable = "memora-cli" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "typer", specifier = ">=0.20.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memora-dev"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue