aegis-memory 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ """
2
+ Aegis Memory - Agent-native memory fabric for AI agents.
3
+
4
+ Aegis Memory is an open-source, self-hostable memory engine for LLM agents with:
5
+ - Agent-native semantics (namespace, scope, multi-agent ACLs)
6
+ - First-class multi-agent support (cross-agent queries, structured handoffs)
7
+ - Context-engineering patterns (ACE-style voting, deltas, reflections, playbooks)
8
+ - Production-oriented design (FastAPI + Postgres + pgvector)
9
+
10
+ Quick Start:
11
+ from aegis_memory import AegisClient
12
+
13
+ client = AegisClient(api_key="your-key", base_url="http://localhost:8000")
14
+
15
+ # Add a memory
16
+ result = client.add("User prefers dark mode", agent_id="ui-agent")
17
+
18
+ # Query memories
19
+ memories = client.query("user preferences", agent_id="ui-agent")
20
+
21
+ # Cross-agent query with access control
22
+ memories = client.query_cross_agent(
23
+ "user settings",
24
+ requesting_agent_id="settings-agent"
25
+ )
26
+
27
+ For more examples, see: https://github.com/quantifylabs/aegis-memory/tree/main/examples
28
+ """
29
+
30
+ __version__ = "1.2.0"
31
+
32
+ from aegis_memory.client import (
33
+ AegisClient,
34
+ Memory,
35
+ PlaybookEntry,
36
+ SessionProgress,
37
+ Feature,
38
+ )
39
+
40
+ __all__ = [
41
+ "AegisClient",
42
+ "Memory",
43
+ "PlaybookEntry",
44
+ "SessionProgress",
45
+ "Feature",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,9 @@
1
+ """
2
+ Aegis Memory CLI
3
+
4
+ Terminal-first interface for Aegis Memory operations.
5
+ """
6
+
7
+ from aegis_memory.cli.main import app
8
+
9
+ __all__ = ["app"]
@@ -0,0 +1,29 @@
1
+ """
2
+ Aegis CLI Commands
3
+
4
+ All command implementations.
5
+ """
6
+
7
+ from aegis_memory.cli.commands.config import app as config_app
8
+ from aegis_memory.cli.commands import (
9
+ status,
10
+ stats,
11
+ memory,
12
+ vote,
13
+ progress,
14
+ features,
15
+ playbook,
16
+ export_import,
17
+ )
18
+
19
+ __all__ = [
20
+ "config_app",
21
+ "status",
22
+ "stats",
23
+ "memory",
24
+ "vote",
25
+ "progress",
26
+ "features",
27
+ "playbook",
28
+ "export_import",
29
+ ]
@@ -0,0 +1,208 @@
1
+ """
2
+ Aegis CLI Config Command
3
+
4
+ Configuration management: init, show, set, use profiles.
5
+ """
6
+
7
+ import typer
8
+ from typing import Optional
9
+ from rich.console import Console
10
+ from rich.prompt import Prompt, Confirm
11
+ from rich.panel import Panel
12
+
13
+ from aegis_memory.cli.utils.config import (
14
+ load_config,
15
+ save_config,
16
+ load_credentials,
17
+ save_credentials,
18
+ get_config_path,
19
+ get_credentials_path,
20
+ get_active_profile,
21
+ set_nested_value,
22
+ get_nested_value,
23
+ )
24
+ from aegis_memory.cli.utils.auth import get_client, get_api_key
25
+ from aegis_memory.cli.utils.output import print_success, print_error, print_warning
26
+
27
+ app = typer.Typer(help="Configuration management")
28
+ console = Console()
29
+
30
+
31
+ @app.command()
32
+ def init(
33
+ non_interactive: bool = typer.Option(False, "--non-interactive", "-y", help="Use defaults without prompting"),
34
+ ):
35
+ """
36
+ Interactive first-run setup.
37
+
38
+ Creates configuration and credentials files.
39
+ """
40
+ console.print("\n[bold]Welcome to Aegis Memory CLI![/bold]\n")
41
+
42
+ config = load_config()
43
+ credentials = load_credentials()
44
+
45
+ if non_interactive:
46
+ # Use defaults
47
+ api_url = "http://localhost:8000"
48
+ api_key = "dev-key"
49
+ namespace = "default"
50
+ agent_id = "cli-user"
51
+ else:
52
+ # Interactive prompts
53
+ api_url = Prompt.ask(
54
+ "Server URL",
55
+ default="http://localhost:8000"
56
+ )
57
+
58
+ api_key = Prompt.ask(
59
+ "API Key",
60
+ password=True,
61
+ default="dev-key"
62
+ )
63
+
64
+ namespace = Prompt.ask(
65
+ "Default namespace",
66
+ default="default"
67
+ )
68
+
69
+ agent_id = Prompt.ask(
70
+ "Default agent ID",
71
+ default="cli-user"
72
+ )
73
+
74
+ # Update config
75
+ config["profiles"]["local"] = {
76
+ "api_url": api_url,
77
+ "api_key_env": "AEGIS_API_KEY",
78
+ "default_namespace": namespace,
79
+ "default_agent_id": agent_id,
80
+ }
81
+ config["default_profile"] = "local"
82
+
83
+ # Update credentials
84
+ if "profiles" not in credentials:
85
+ credentials["profiles"] = {}
86
+ credentials["profiles"]["local"] = {"api_key": api_key}
87
+
88
+ # Save
89
+ save_config(config)
90
+ save_credentials(credentials)
91
+
92
+ print_success(f"Configuration saved to {get_config_path()}")
93
+ print_success(f"Credentials saved to {get_credentials_path()}")
94
+
95
+ # Test connection
96
+ if not non_interactive:
97
+ if Confirm.ask("\nTest connection?", default=True):
98
+ _test_connection()
99
+ else:
100
+ _test_connection()
101
+
102
+
103
+ def _test_connection():
104
+ """Test connection to server."""
105
+ try:
106
+ client = get_client()
107
+ if client is None:
108
+ print_error("No API key configured")
109
+ return
110
+
111
+ response = client.client.get("/health")
112
+ response.raise_for_status()
113
+ health = response.json()
114
+
115
+ version = health.get("version", "unknown")
116
+ status = health.get("status", "unknown")
117
+
118
+ if status == "healthy":
119
+ print_success(f"Connected to Aegis v{version} ({status})")
120
+ else:
121
+ print_warning(f"Connected to Aegis v{version} ({status})")
122
+
123
+ except Exception as e:
124
+ print_error(f"Connection failed: {str(e)}")
125
+
126
+
127
+ @app.command()
128
+ def show():
129
+ """Show current configuration."""
130
+ config = load_config()
131
+ profile_name = config.get("default_profile", "local")
132
+ profile = get_active_profile(config)
133
+
134
+ console.print(f"\n[bold]Profile:[/bold] {profile_name} (active)")
135
+ console.print(f"[bold]API URL:[/bold] {profile.get('api_url', 'not set')}")
136
+ console.print(f"[bold]Namespace:[/bold] {profile.get('default_namespace', 'default')}")
137
+ console.print(f"[bold]Agent ID:[/bold] {profile.get('default_agent_id', 'cli-user')}")
138
+
139
+ output_config = config.get("output", {})
140
+ console.print(f"[bold]Output:[/bold] {output_config.get('format', 'table')}")
141
+
142
+ # Show API key status (masked)
143
+ api_key = get_api_key(config)
144
+ if api_key:
145
+ masked = api_key[:4] + "..." + api_key[-4:] if len(api_key) > 8 else "****"
146
+ console.print(f"[bold]API Key:[/bold] {masked}")
147
+ else:
148
+ console.print("[bold]API Key:[/bold] [red]not configured[/red]")
149
+
150
+ console.print(f"\n[dim]Config: {get_config_path()}[/dim]")
151
+
152
+
153
+ @app.command("set")
154
+ def set_value(
155
+ key: str = typer.Argument(..., help="Config key (e.g., profiles.local.api_url)"),
156
+ value: str = typer.Argument(..., help="Value to set"),
157
+ ):
158
+ """Set a configuration value."""
159
+ config = load_config()
160
+
161
+ keys = key.split(".")
162
+ set_nested_value(config, keys, value)
163
+ save_config(config)
164
+
165
+ print_success(f"Set {key} = {value}")
166
+
167
+
168
+ @app.command()
169
+ def use(
170
+ profile: str = typer.Argument(..., help="Profile name to switch to"),
171
+ ):
172
+ """Switch to a different profile."""
173
+ config = load_config()
174
+
175
+ if profile not in config.get("profiles", {}):
176
+ print_error(f"Profile '{profile}' not found")
177
+ console.print("\nAvailable profiles:")
178
+ for name in config.get("profiles", {}).keys():
179
+ console.print(f" - {name}")
180
+ raise typer.Exit(1)
181
+
182
+ config["default_profile"] = profile
183
+ save_config(config)
184
+
185
+ print_success(f"Switched to profile: {profile}")
186
+
187
+
188
+ @app.command()
189
+ def profiles():
190
+ """List all configured profiles."""
191
+ config = load_config()
192
+ current = config.get("default_profile", "local")
193
+
194
+ console.print("\n[bold]Configured Profiles[/bold]\n")
195
+
196
+ for name, profile in config.get("profiles", {}).items():
197
+ marker = "[green]●[/green]" if name == current else "[dim]○[/dim]"
198
+ console.print(f" {marker} {name}")
199
+ console.print(f" URL: {profile.get('api_url', 'not set')}")
200
+ console.print(f" Namespace: {profile.get('default_namespace', 'default')}")
201
+ console.print()
202
+
203
+
204
+ @app.command()
205
+ def path():
206
+ """Show configuration file paths."""
207
+ console.print(f"[bold]Config:[/bold] {get_config_path()}")
208
+ console.print(f"[bold]Credentials:[/bold] {get_credentials_path()}")
@@ -0,0 +1,205 @@
1
+ """
2
+ Aegis CLI Export/Import Commands
3
+
4
+ Data export and import for backup and migration.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ import typer
10
+ from typing import Optional
11
+ from pathlib import Path
12
+ from rich.console import Console
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn
14
+
15
+ from aegis_memory.cli.utils.auth import get_client, get_default_namespace
16
+ from aegis_memory.cli.utils.output import print_json, print_success, print_error
17
+ from aegis_memory.cli.utils.errors import wrap_errors, require_client, handle_api_error
18
+
19
+ console = Console()
20
+
21
+
22
+ @wrap_errors
23
+ def export(
24
+ namespace: Optional[str] = typer.Option(None, "--namespace", "-n", help="Filter by namespace (default: all)"),
25
+ agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Filter by agent ID"),
26
+ format: str = typer.Option("jsonl", "--format", "-f", help="Format: jsonl or json"),
27
+ include_embeddings: bool = typer.Option(False, "--include-embeddings", help="Include embedding vectors"),
28
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file (default: stdout)"),
29
+ limit: Optional[int] = typer.Option(None, "--limit", help="Max memories to export"),
30
+ ):
31
+ """
32
+ Export memories for backup or migration.
33
+
34
+ Examples:
35
+ aegis export > backup.jsonl
36
+ aegis export -o backup.jsonl
37
+ aegis export -n production -f json -o prod-backup.json
38
+ aegis export --include-embeddings -o full-backup.jsonl
39
+ """
40
+ if format not in ("jsonl", "json"):
41
+ print_error("Format must be 'jsonl' or 'json'")
42
+ raise typer.Exit(1)
43
+
44
+ client = require_client()
45
+
46
+ # Build request
47
+ params = {"format": format}
48
+ if namespace:
49
+ params["namespace"] = namespace
50
+ if agent:
51
+ params["agent_id"] = agent
52
+ if include_embeddings:
53
+ params["include_embeddings"] = True
54
+ if limit:
55
+ params["limit"] = limit
56
+
57
+ with Progress(
58
+ SpinnerColumn(),
59
+ TextColumn("[progress.description]{task.description}"),
60
+ console=console,
61
+ transient=True,
62
+ ) as progress:
63
+ progress.add_task(description="Exporting memories...", total=None)
64
+
65
+ try:
66
+ response = client.client.post("/memories/export", json=params)
67
+ response.raise_for_status()
68
+ except Exception as e:
69
+ handle_api_error(e, "export")
70
+
71
+ # Handle output
72
+ if format == "jsonl":
73
+ # Streaming response
74
+ content = response.text
75
+ lines = content.strip().split("\n") if content.strip() else []
76
+ count = len(lines)
77
+
78
+ if output:
79
+ output.write_text(content)
80
+ print_success(f"Exported {count} memories to {output}")
81
+ else:
82
+ # Output to stdout
83
+ console.print(content, end="")
84
+ if sys.stdout.isatty():
85
+ console.print(f"\n[dim]Exported {count} memories[/dim]", err=True)
86
+ else:
87
+ # JSON response
88
+ data = response.json()
89
+ memories = data.get("memories", [])
90
+ count = len(memories)
91
+
92
+ if output:
93
+ with open(output, "w") as f:
94
+ json.dump(data, f, indent=2, default=str)
95
+ print_success(f"Exported {count} memories to {output}")
96
+ else:
97
+ console.print(json.dumps(data, indent=2, default=str))
98
+ if sys.stdout.isatty():
99
+ console.print(f"\n[dim]Exported {count} memories[/dim]", err=True)
100
+
101
+
102
+ @wrap_errors
103
+ def import_memories(
104
+ file: Path = typer.Argument(..., help="File to import from"),
105
+ namespace: Optional[str] = typer.Option(None, "--namespace", "-n", help="Override namespace"),
106
+ agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Override agent ID"),
107
+ skip_duplicates: bool = typer.Option(True, "--skip-duplicates/--no-skip-duplicates", help="Skip content duplicates"),
108
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate without importing"),
109
+ ):
110
+ """
111
+ Import memories from export file.
112
+
113
+ Examples:
114
+ aegis import backup.jsonl
115
+ aegis import backup.json -n staging
116
+ aegis import backup.jsonl --dry-run
117
+ """
118
+ if not file.exists():
119
+ print_error(f"File not found: {file}")
120
+ raise typer.Exit(1)
121
+
122
+ client = require_client()
123
+
124
+ # Detect format
125
+ content = file.read_text()
126
+
127
+ if file.suffix == ".json" or content.strip().startswith("{"):
128
+ # JSON format
129
+ try:
130
+ data = json.loads(content)
131
+ memories = data.get("memories", [])
132
+ except json.JSONDecodeError as e:
133
+ print_error(f"Invalid JSON: {e}")
134
+ raise typer.Exit(1)
135
+ else:
136
+ # JSONL format
137
+ memories = []
138
+ for line in content.strip().split("\n"):
139
+ if line.strip():
140
+ try:
141
+ memories.append(json.loads(line))
142
+ except json.JSONDecodeError as e:
143
+ print_error(f"Invalid JSONL line: {e}")
144
+ raise typer.Exit(1)
145
+
146
+ if not memories:
147
+ print_error("No memories found in file")
148
+ raise typer.Exit(1)
149
+
150
+ if dry_run:
151
+ console.print(f"\n[bold]Dry run[/bold] - would import {len(memories)} memories")
152
+
153
+ # Show sample
154
+ if memories:
155
+ sample = memories[0]
156
+ console.print(f"\nSample memory:")
157
+ console.print(f" Content: {sample.get('content', '')[:60]}...")
158
+ console.print(f" Agent: {namespace or sample.get('agent_id', '-')}")
159
+ console.print(f" Namespace: {agent or sample.get('namespace', 'default')}")
160
+
161
+ return
162
+
163
+ # Import memories
164
+ imported = 0
165
+ skipped = 0
166
+ errors = 0
167
+
168
+ with Progress(
169
+ SpinnerColumn(),
170
+ TextColumn("[progress.description]{task.description}"),
171
+ console=console,
172
+ ) as progress:
173
+ task = progress.add_task(description="Importing...", total=len(memories))
174
+
175
+ for mem in memories:
176
+ try:
177
+ # Override namespace/agent if specified
178
+ mem_namespace = namespace or mem.get("namespace", "default")
179
+ mem_agent = agent or mem.get("agent_id")
180
+
181
+ result = client.add(
182
+ content=mem.get("content", ""),
183
+ agent_id=mem_agent,
184
+ user_id=mem.get("user_id"),
185
+ namespace=mem_namespace,
186
+ scope=mem.get("scope"),
187
+ metadata=mem.get("metadata"),
188
+ )
189
+
190
+ if result.deduped_from and skip_duplicates:
191
+ skipped += 1
192
+ else:
193
+ imported += 1
194
+
195
+ except Exception as e:
196
+ errors += 1
197
+
198
+ progress.update(task, advance=1)
199
+
200
+ # Summary
201
+ print_success(f"Imported {imported} memories")
202
+ if skipped:
203
+ console.print(f" [dim]Skipped (duplicate): {skipped}[/dim]")
204
+ if errors:
205
+ console.print(f" [yellow]Errors: {errors}[/yellow]")