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"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
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"
|
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">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">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">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>
|
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -94,11 +93,6 @@ export function DocumentsView() {
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 border border-border">{doc.text_length?.toLocaleString()} chars</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">{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">
|
<td className="p-2 border border-border">
|
||||||
<button
|
<button
|
||||||
onClick={() => viewDocumentText(doc.id)}
|
onClick={() => viewDocumentText(doc.id)}
|
||||||
|
|
@ -112,7 +106,7 @@ export function DocumentsView() {
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<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
|
Click "Load Documents" to view data
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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):
|
class SearchRequest(BaseModel):
|
||||||
"""Request model for search endpoint."""
|
"""Request model for search endpoint."""
|
||||||
query: str
|
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"
|
agent_id: str = "default"
|
||||||
thinking_budget: int = 100
|
thinking_budget: int = 100
|
||||||
max_tokens: int = 4096
|
max_tokens: int = 4096
|
||||||
|
|
@ -274,7 +274,6 @@ class ListDocumentsResponse(BaseModel):
|
||||||
"id": "session_1",
|
"id": "session_1",
|
||||||
"agent_id": "user123",
|
"agent_id": "user123",
|
||||||
"content_hash": "abc123",
|
"content_hash": "abc123",
|
||||||
"metadata": {"source": "conversation"},
|
|
||||||
"created_at": "2024-01-15T10:30:00Z",
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
"updated_at": "2024-01-15T10:30:00Z",
|
"updated_at": "2024-01-15T10:30:00Z",
|
||||||
"text_length": 5420,
|
"text_length": 5420,
|
||||||
|
|
@ -294,7 +293,6 @@ class DocumentResponse(BaseModel):
|
||||||
agent_id: str
|
agent_id: str
|
||||||
original_text: str
|
original_text: str
|
||||||
content_hash: Optional[str]
|
content_hash: Optional[str]
|
||||||
metadata: Dict[str, Any]
|
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
memory_unit_count: int
|
memory_unit_count: int
|
||||||
|
|
@ -306,7 +304,6 @@ class DocumentResponse(BaseModel):
|
||||||
"agent_id": "user123",
|
"agent_id": "user123",
|
||||||
"original_text": "Full document text here...",
|
"original_text": "Full document text here...",
|
||||||
"content_hash": "abc123",
|
"content_hash": "abc123",
|
||||||
"metadata": {"source": "conversation"},
|
|
||||||
"created_at": "2024-01-15T10:30:00Z",
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
"updated_at": "2024-01-15T10:30:00Z",
|
"updated_at": "2024-01-15T10:30:00Z",
|
||||||
"memory_unit_count": 15
|
"memory_unit_count": 15
|
||||||
|
|
@ -380,15 +377,6 @@ The system uses:
|
||||||
def _register_routes(app: FastAPI):
|
def _register_routes(app: FastAPI):
|
||||||
"""Register all API routes on the given app instance."""
|
"""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(
|
@app.get(
|
||||||
"/api/graph",
|
"/api/graph",
|
||||||
|
|
@ -472,18 +460,16 @@ def _register_routes(app: FastAPI):
|
||||||
# Validate fact_type(s)
|
# Validate fact_type(s)
|
||||||
valid_fact_types = ["world", "agent", "opinion"]
|
valid_fact_types = ["world", "agent", "opinion"]
|
||||||
|
|
||||||
|
# Default to all fact types if not specified
|
||||||
if not request.fact_type:
|
if not request.fact_type:
|
||||||
raise HTTPException(
|
request.fact_type = valid_fact_types
|
||||||
status_code=400,
|
else:
|
||||||
detail="fact_type must be a non-empty list"
|
for ft in request.fact_type:
|
||||||
)
|
if ft not in valid_fact_types:
|
||||||
|
raise HTTPException(
|
||||||
for ft in request.fact_type:
|
status_code=400,
|
||||||
if ft not in valid_fact_types:
|
detail=f"Invalid fact_type '{ft}'. Must be one of: {', '.join(valid_fact_types)}"
|
||||||
raise HTTPException(
|
)
|
||||||
status_code=400,
|
|
||||||
detail=f"Invalid fact_type '{ft}'. Must be one of: {', '.join(valid_fact_types)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse question_date if provided
|
# Parse question_date if provided
|
||||||
question_date = None
|
question_date = None
|
||||||
|
|
@ -508,11 +494,22 @@ def _register_routes(app: FastAPI):
|
||||||
question_date=question_date
|
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
|
# Convert trace to dict
|
||||||
trace_dict = trace.to_dict() if trace else None
|
trace_dict = trace.to_dict() if trace else None
|
||||||
|
|
||||||
return SearchResponse(
|
return SearchResponse(
|
||||||
results=results,
|
results=filtered_results,
|
||||||
trace=trace_dict
|
trace=trace_dict
|
||||||
)
|
)
|
||||||
except HTTPException:
|
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."
|
description="List documents with pagination and optional search. Documents are the source content from which memory units are extracted."
|
||||||
)
|
)
|
||||||
async def api_list_documents(
|
async def api_list_documents(
|
||||||
agent_id: Optional[str] = None,
|
agent_id: str,
|
||||||
q: Optional[str] = None,
|
q: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0
|
offset: int = 0
|
||||||
|
|
@ -681,7 +678,7 @@ def _register_routes(app: FastAPI):
|
||||||
List documents for an agent with optional search.
|
List documents for an agent with optional search.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_id: Filter by agent ID
|
agent_id: Agent ID (required)
|
||||||
q: Search query (searches document ID and metadata)
|
q: Search query (searches document ID and metadata)
|
||||||
limit: Maximum number of results (default: 100)
|
limit: Maximum number of results (default: 100)
|
||||||
offset: Offset for pagination (default: 0)
|
offset: Offset for pagination (default: 0)
|
||||||
|
|
@ -760,14 +757,6 @@ def _register_routes(app: FastAPI):
|
||||||
)
|
)
|
||||||
async def api_batch_put(request: BatchPutRequest):
|
async def api_batch_put(request: BatchPutRequest):
|
||||||
try:
|
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
|
# Prepare contents for put_batch_async
|
||||||
contents = []
|
contents = []
|
||||||
for item in request.items:
|
for item in request.items:
|
||||||
|
|
@ -784,7 +773,7 @@ def _register_routes(app: FastAPI):
|
||||||
contents=contents,
|
contents=contents,
|
||||||
document_id=request.document_id
|
document_id=request.document_id
|
||||||
)
|
)
|
||||||
logging.info(f"Batch put result: {result}")
|
|
||||||
|
|
||||||
return BatchPutResponse(
|
return BatchPutResponse(
|
||||||
success=True,
|
success=True,
|
||||||
|
|
@ -830,14 +819,6 @@ def _register_routes(app: FastAPI):
|
||||||
)
|
)
|
||||||
async def api_batch_put_async(request: BatchPutRequest):
|
async def api_batch_put_async(request: BatchPutRequest):
|
||||||
try:
|
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
|
# Prepare contents for put_batch_async
|
||||||
contents = []
|
contents = []
|
||||||
for item in request.items:
|
for item in request.items:
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,15 @@ class ThinkOperationsMixin:
|
||||||
fact_type=['agent', 'world', 'opinion']
|
fact_type=['agent', 'world', 'opinion']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"[THINK] Search returned {len(all_results)} results")
|
||||||
|
|
||||||
# Split results by fact type for structured response
|
# Split results by fact type for structured response
|
||||||
agent_results = [r for r in all_results if r.get('fact_type') == 'agent']
|
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']
|
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']
|
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
|
# Step 4: Format facts for LLM with full details as JSON
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
@ -99,6 +103,8 @@ class ThinkOperationsMixin:
|
||||||
world_facts_text = format_facts(world_results)
|
world_facts_text = format_facts(world_results)
|
||||||
opinion_facts_text = format_facts(opinion_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
|
# 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.
|
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."""
|
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(
|
answer_text = await self._llm_config.call(
|
||||||
messages=[
|
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."},
|
{"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"],
|
"text": data["text"],
|
||||||
"context": data.get("context", ""),
|
"context": data.get("context", ""),
|
||||||
"event_date": data["event_date"], # Keep as datetime for now
|
"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),
|
"access_count": data.get("access_count", 0),
|
||||||
"semantic_similarity": semantic_sim,
|
"semantic_similarity": semantic_sim,
|
||||||
"bm25_score": bm25_score,
|
"bm25_score": bm25_score,
|
||||||
|
|
@ -1447,12 +1448,12 @@ class TemporalSemanticMemory(
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
doc = await conn.fetchrow(
|
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
|
d.created_at, d.updated_at, COUNT(mu.id) as unit_count
|
||||||
FROM documents d
|
FROM documents d
|
||||||
LEFT JOIN memory_units mu ON mu.document_id = d.id
|
LEFT JOIN memory_units mu ON mu.document_id = d.id
|
||||||
WHERE d.id = $1 AND d.agent_id = $2
|
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
|
document_id, agent_id
|
||||||
)
|
)
|
||||||
|
|
@ -1460,13 +1461,11 @@ class TemporalSemanticMemory(
|
||||||
if not doc:
|
if not doc:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
import json
|
|
||||||
return {
|
return {
|
||||||
"id": doc["id"],
|
"id": doc["id"],
|
||||||
"agent_id": doc["agent_id"],
|
"agent_id": doc["agent_id"],
|
||||||
"original_text": doc["original_text"],
|
"original_text": doc["original_text"],
|
||||||
"content_hash": doc["content_hash"],
|
"content_hash": doc["content_hash"],
|
||||||
"metadata": json.loads(doc["metadata"]) if doc["metadata"] else {},
|
|
||||||
"unit_count": doc["unit_count"],
|
"unit_count": doc["unit_count"],
|
||||||
"created_at": doc["created_at"],
|
"created_at": doc["created_at"],
|
||||||
"updated_at": doc["updated_at"]
|
"updated_at": doc["updated_at"]
|
||||||
|
|
@ -1871,7 +1870,7 @@ class TemporalSemanticMemory(
|
||||||
|
|
||||||
async def list_documents(
|
async def list_documents(
|
||||||
self,
|
self,
|
||||||
agent_id: Optional[str] = None,
|
agent_id: str,
|
||||||
search_query: Optional[str] = None,
|
search_query: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0
|
offset: int = 0
|
||||||
|
|
@ -1880,8 +1879,8 @@ class TemporalSemanticMemory(
|
||||||
List documents with optional search and pagination.
|
List documents with optional search and pagination.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_id: Filter by agent ID
|
agent_id: Agent ID (required)
|
||||||
search_query: Search in metadata (JSON text search)
|
search_query: Search in document ID
|
||||||
limit: Maximum number of results
|
limit: Maximum number of results
|
||||||
offset: Offset for pagination
|
offset: Offset for pagination
|
||||||
|
|
||||||
|
|
@ -1895,15 +1894,14 @@ class TemporalSemanticMemory(
|
||||||
query_params = []
|
query_params = []
|
||||||
param_count = 0
|
param_count = 0
|
||||||
|
|
||||||
if agent_id:
|
param_count += 1
|
||||||
param_count += 1
|
query_conditions.append(f"agent_id = ${param_count}")
|
||||||
query_conditions.append(f"agent_id = ${param_count}")
|
query_params.append(agent_id)
|
||||||
query_params.append(agent_id)
|
|
||||||
|
|
||||||
if search_query:
|
if search_query:
|
||||||
# Search in document ID and metadata (as text)
|
# Search in document ID
|
||||||
param_count += 1
|
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}%")
|
query_params.append(f"%{search_query}%")
|
||||||
|
|
||||||
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
|
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
|
||||||
|
|
@ -1931,7 +1929,6 @@ class TemporalSemanticMemory(
|
||||||
id,
|
id,
|
||||||
agent_id,
|
agent_id,
|
||||||
content_hash,
|
content_hash,
|
||||||
metadata,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
LENGTH(original_text) as text_length
|
LENGTH(original_text) as text_length
|
||||||
|
|
@ -1979,7 +1976,6 @@ class TemporalSemanticMemory(
|
||||||
"id": doc_id,
|
"id": doc_id,
|
||||||
"agent_id": agent_id_val,
|
"agent_id": agent_id_val,
|
||||||
"content_hash": row['content_hash'],
|
"content_hash": row['content_hash'],
|
||||||
"metadata": row['metadata'] if row['metadata'] else {},
|
|
||||||
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
|
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
|
||||||
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
|
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
|
||||||
"text_length": row['text_length'] or 0,
|
"text_length": row['text_length'] or 0,
|
||||||
|
|
@ -2016,7 +2012,6 @@ class TemporalSemanticMemory(
|
||||||
agent_id,
|
agent_id,
|
||||||
original_text,
|
original_text,
|
||||||
content_hash,
|
content_hash,
|
||||||
metadata,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM documents
|
FROM documents
|
||||||
|
|
@ -2038,7 +2033,6 @@ class TemporalSemanticMemory(
|
||||||
"agent_id": doc['agent_id'],
|
"agent_id": doc['agent_id'],
|
||||||
"original_text": doc['original_text'],
|
"original_text": doc['original_text'],
|
||||||
"content_hash": doc['content_hash'],
|
"content_hash": doc['content_hash'],
|
||||||
"metadata": doc['metadata'] if doc['metadata'] else {},
|
|
||||||
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
|
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
|
||||||
"updated_at": doc['updated_at'].isoformat() if doc['updated_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
|
"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]
|
[tool.uv.workspace]
|
||||||
members = ["memora", "benchmarks", "memora-dev"]
|
members = ["memora", "benchmarks", "memora-dev", "memora-cli"]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
dev-dependencies = []
|
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 = [
|
members = [
|
||||||
"benchmarks",
|
"benchmarks",
|
||||||
"memora",
|
"memora",
|
||||||
|
"memora-cli",
|
||||||
"memora-dev",
|
"memora-dev",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1093,6 +1094,23 @@ requires-dist = [
|
||||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
{ 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]]
|
[[package]]
|
||||||
name = "memora-dev"
|
name = "memora-dev"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue