Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +53 -7
- app.py +443 -0
- demo_data/.seeded +1 -0
- demo_data/config.json +18 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/inbox/33d7dea804da.txt +15 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/notes/handoff.txt +1 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/architecture.txt +1 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/capabilities.txt +1 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/observation-patterns.txt +1 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
- demo_data/mailboxes/demo-agent@localhost/2026-03-08/who_am_i/identity.txt +1 -0
- demo_data/mailboxes/helper-agent@localhost/2026-03-08/remember/agentazall-design.txt +1 -0
- demo_data/mailboxes/helper-agent@localhost/2026-03-08/sent/33d7dea804da.txt +15 -0
- demo_data/mailboxes/helper-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
- demo_data/mailboxes/helper-agent@localhost/2026-03-08/who_am_i/identity.txt +1 -0
- demo_data/mailboxes/visitor@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
- demo_data/mailboxes/visitor@localhost/2026-03-08/who_am_i/identity.txt +1 -0
- llm_bridge.py +414 -0
- requirements.txt +5 -0
- seed_data.py +218 -0
- src/agentazall/__init__.py +3 -0
- src/agentazall/commands/__init__.py +1 -0
- src/agentazall/commands/identity.py +49 -0
- src/agentazall/commands/memory.py +106 -0
- src/agentazall/commands/messaging.py +220 -0
- src/agentazall/commands/notes.py +53 -0
- src/agentazall/config.py +140 -0
- src/agentazall/finder.py +59 -0
- src/agentazall/helpers.py +122 -0
- src/agentazall/index.py +222 -0
- src/agentazall/messages.py +60 -0
README.md
CHANGED
|
@@ -1,13 +1,59 @@
|
|
| 1 |
---
|
| 2 |
-
title: AgentAZAll
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AgentAZAll - Persistent Memory for LLM Agents
|
| 3 |
+
emoji: "\U0001F9E0"
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "5.23.0"
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
short_description: "Give LLM agents memory that survives across sessions"
|
| 12 |
+
tags:
|
| 13 |
+
- agent
|
| 14 |
+
- memory
|
| 15 |
+
- multi-agent
|
| 16 |
+
- persistent-memory
|
| 17 |
+
- tool-use
|
| 18 |
+
models:
|
| 19 |
+
- HuggingFaceTB/SmolLM2-1.7B-Instruct
|
| 20 |
+
preload_from_hub:
|
| 21 |
+
- HuggingFaceTB/SmolLM2-1.7B-Instruct
|
| 22 |
---
|
| 23 |
|
| 24 |
+
# AgentAZAll β Persistent Memory for LLM Agents
|
| 25 |
+
|
| 26 |
+
Chat with an AI agent that actually **remembers**. This demo runs
|
| 27 |
+
[SmolLM2-1.7B-Instruct](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct)
|
| 28 |
+
on ZeroGPU, powered by [AgentAZAll](https://github.com/gregorkoch/agentazall) β
|
| 29 |
+
a file-based persistent memory and communication system for LLM agents.
|
| 30 |
+
|
| 31 |
+
## What You Can Do
|
| 32 |
+
|
| 33 |
+
- **Chat** with an agent that stores and recalls memories across messages
|
| 34 |
+
- **Send messages** between agents in a simulated multi-agent network
|
| 35 |
+
- **Browse** the agent dashboard to see memories, inbox, and identity
|
| 36 |
+
- **Watch** the agent use tools in real time (remember, recall, send, inbox)
|
| 37 |
+
|
| 38 |
+
## How It Works
|
| 39 |
+
|
| 40 |
+
AgentAZAll gives every agent a file-based mailbox with:
|
| 41 |
+
- **Persistent memory** (`remember` / `recall`) that survives context resets
|
| 42 |
+
- **Inter-agent messaging** (`send` / `inbox` / `reply`)
|
| 43 |
+
- **Identity continuity** (`whoami` / `doing`)
|
| 44 |
+
- **Working notes** for ongoing projects
|
| 45 |
+
|
| 46 |
+
No database required β everything is plain text files organized by date.
|
| 47 |
+
|
| 48 |
+
## Install Locally
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
pip install agentazall
|
| 52 |
+
agentazall setup --agent my-agent@localhost
|
| 53 |
+
agentazall remember --text "Important fact" --title "my-fact"
|
| 54 |
+
agentazall recall
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## License
|
| 58 |
+
|
| 59 |
+
GPL-3.0-or-later
|
app.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll HuggingFace Spaces Demo.
|
| 2 |
+
|
| 3 |
+
A live demo of persistent memory for LLM agents, powered by SmolLM2-1.7B-Instruct
|
| 4 |
+
on ZeroGPU and the AgentAZAll file-based memory system.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Ensure src/ is importable
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
|
| 15 |
+
from seed_data import (
|
| 16 |
+
AGENTS,
|
| 17 |
+
MAILBOXES,
|
| 18 |
+
make_demo_config,
|
| 19 |
+
reset_demo_data,
|
| 20 |
+
seed_demo_data,
|
| 21 |
+
)
|
| 22 |
+
from llm_bridge import (
|
| 23 |
+
_tool_directory,
|
| 24 |
+
_tool_inbox,
|
| 25 |
+
_tool_recall,
|
| 26 |
+
_tool_whoami,
|
| 27 |
+
_tool_doing,
|
| 28 |
+
_tool_note,
|
| 29 |
+
_tool_remember,
|
| 30 |
+
_tool_send,
|
| 31 |
+
chat_with_agent,
|
| 32 |
+
)
|
| 33 |
+
from agentazall.helpers import today_str
|
| 34 |
+
from agentazall.config import INBOX, NOTES, REMEMBER, SENT
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
# Initialize
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
|
| 40 |
+
seed_demo_data()
|
| 41 |
+
DEMO_CFG = make_demo_config("demo-agent@localhost")
|
| 42 |
+
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
# Chat tab functions
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def agent_chat(message: str, history: list) -> str:
|
| 49 |
+
"""Chat with the demo agent."""
|
| 50 |
+
if not message or not message.strip():
|
| 51 |
+
return "Please type a message."
|
| 52 |
+
try:
|
| 53 |
+
return chat_with_agent(message.strip(), history, DEMO_CFG)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
return f"Error: {e}\n\n(This may happen if GPU quota is exhausted. Try again later.)"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def get_memory_sidebar() -> str:
|
| 59 |
+
"""Get current memory state for the sidebar."""
|
| 60 |
+
return _tool_recall(DEMO_CFG, [])
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------------------------------------
|
| 64 |
+
# Dashboard tab functions
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_directory() -> str:
|
| 69 |
+
return _tool_directory(DEMO_CFG, [])
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_agent_memories(agent_name: str) -> str:
|
| 73 |
+
if not agent_name:
|
| 74 |
+
return "Select an agent."
|
| 75 |
+
cfg = make_demo_config(agent_name)
|
| 76 |
+
return _tool_recall(cfg, [])
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_agent_inbox(agent_name: str) -> str:
|
| 80 |
+
if not agent_name:
|
| 81 |
+
return "Select an agent."
|
| 82 |
+
cfg = make_demo_config(agent_name)
|
| 83 |
+
return _tool_inbox(cfg, [])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_agent_identity(agent_name: str) -> str:
|
| 87 |
+
if not agent_name:
|
| 88 |
+
return "Select an agent."
|
| 89 |
+
cfg = make_demo_config(agent_name)
|
| 90 |
+
identity = _tool_whoami(cfg, [])
|
| 91 |
+
doing = _tool_doing(cfg, [])
|
| 92 |
+
return f"**Identity:** {identity}\n\n**Current task:** {doing}"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def get_agent_notes(agent_name: str) -> str:
|
| 96 |
+
if not agent_name:
|
| 97 |
+
return "Select an agent."
|
| 98 |
+
cfg = make_demo_config(agent_name)
|
| 99 |
+
d = today_str()
|
| 100 |
+
notes_dir = Path(cfg["mailbox_dir"]) / agent_name / d / NOTES
|
| 101 |
+
if not notes_dir.exists():
|
| 102 |
+
return "No notes."
|
| 103 |
+
notes = []
|
| 104 |
+
for f in sorted(notes_dir.iterdir()):
|
| 105 |
+
if f.is_file() and f.suffix == ".txt":
|
| 106 |
+
content = f.read_text(encoding="utf-8").strip()[:200]
|
| 107 |
+
notes.append(f"**{f.stem}:** {content}")
|
| 108 |
+
return "\n\n".join(notes) if notes else "No notes."
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def manual_remember(agent_name: str, text: str, title: str) -> str:
|
| 112 |
+
if not agent_name or not text.strip():
|
| 113 |
+
return "Need agent and text."
|
| 114 |
+
cfg = make_demo_config(agent_name)
|
| 115 |
+
args = [text.strip()]
|
| 116 |
+
if title.strip():
|
| 117 |
+
args.append(title.strip())
|
| 118 |
+
return _tool_remember(cfg, args)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def manual_send(from_agent: str, to_agent: str, subject: str, body: str) -> str:
|
| 122 |
+
if not all([from_agent, to_agent, subject.strip(), body.strip()]):
|
| 123 |
+
return "All fields required."
|
| 124 |
+
cfg = make_demo_config(from_agent)
|
| 125 |
+
return _tool_send(cfg, [to_agent, subject.strip(), body.strip()])
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def do_reset() -> str:
|
| 129 |
+
return reset_demo_data()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ---------------------------------------------------------------------------
|
| 133 |
+
# Agent name list for dropdowns
|
| 134 |
+
# ---------------------------------------------------------------------------
|
| 135 |
+
|
| 136 |
+
AGENT_NAMES = list(AGENTS.keys())
|
| 137 |
+
|
| 138 |
+
# ---------------------------------------------------------------------------
|
| 139 |
+
# Build Gradio UI
|
| 140 |
+
# ---------------------------------------------------------------------------
|
| 141 |
+
|
| 142 |
+
CSS = """
|
| 143 |
+
.memory-sidebar { font-size: 0.85em; }
|
| 144 |
+
.tool-result { background: #f0f4f8; padding: 8px; border-radius: 4px; margin: 4px 0; }
|
| 145 |
+
footer { display: none !important; }
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
HOW_IT_WORKS_MD = """\
|
| 149 |
+
## How AgentAZAll Works
|
| 150 |
+
|
| 151 |
+
AgentAZAll is a **file-based persistent memory and communication system** for LLM agents.
|
| 152 |
+
Every agent gets a mailbox directory organized by date:
|
| 153 |
+
|
| 154 |
+
```
|
| 155 |
+
data/mailboxes/
|
| 156 |
+
demo-agent@localhost/
|
| 157 |
+
2026-03-08/
|
| 158 |
+
inbox/ # received messages
|
| 159 |
+
sent/ # delivered messages
|
| 160 |
+
who_am_i/ # identity.txt
|
| 161 |
+
what_am_i_doing/ # tasks.txt
|
| 162 |
+
remember/ # persistent memories
|
| 163 |
+
notes/ # working notes
|
| 164 |
+
skills/ # reusable Python scripts
|
| 165 |
+
tools/ # reusable tools
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### Key Features
|
| 169 |
+
|
| 170 |
+
| Feature | Commands | Description |
|
| 171 |
+
|---------|----------|-------------|
|
| 172 |
+
| **Persistent Memory** | `remember`, `recall` | Store and search memories that survive context resets |
|
| 173 |
+
| **Inter-Agent Messaging** | `send`, `inbox`, `reply` | Agents communicate via email-like messages |
|
| 174 |
+
| **Identity Continuity** | `whoami`, `doing` | Maintain identity and task state across sessions |
|
| 175 |
+
| **Working Notes** | `note`, `notes` | Named notes for ongoing projects |
|
| 176 |
+
| **Agent Directory** | `directory` | Discover other agents in the network |
|
| 177 |
+
| **Daily Index** | `index` | Auto-generated summary of each day's activity |
|
| 178 |
+
|
| 179 |
+
### Integration with LLM Agents
|
| 180 |
+
|
| 181 |
+
Add this to your agent's system prompt (e.g., `CLAUDE.md`):
|
| 182 |
+
|
| 183 |
+
```bash
|
| 184 |
+
# At session start -- restore context:
|
| 185 |
+
agentazall recall # what do I remember?
|
| 186 |
+
agentazall whoami # who am I?
|
| 187 |
+
agentazall doing # what was I doing?
|
| 188 |
+
agentazall inbox # any new messages?
|
| 189 |
+
|
| 190 |
+
# During work -- save important observations:
|
| 191 |
+
agentazall remember --text "Important insight" --title "my-observation"
|
| 192 |
+
|
| 193 |
+
# Before context runs low -- save state:
|
| 194 |
+
agentazall doing --set "CURRENT: X. NEXT: Y."
|
| 195 |
+
agentazall note handoff --set "detailed state for next session"
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
### Install Locally
|
| 199 |
+
|
| 200 |
+
```bash
|
| 201 |
+
pip install agentazall
|
| 202 |
+
agentazall setup --agent my-agent@localhost
|
| 203 |
+
agentazall remember --text "Hello world" --title "first-memory"
|
| 204 |
+
agentazall recall
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Architecture
|
| 208 |
+
|
| 209 |
+
- **Zero external dependencies** for core (Python stdlib only)
|
| 210 |
+
- **File-based storage** -- no database, fully portable
|
| 211 |
+
- **Email transport** (SMTP/IMAP/POP3) for remote agent communication
|
| 212 |
+
- **FTP transport** as alternative
|
| 213 |
+
- **Gradio web UI** for human participants
|
| 214 |
+
|
| 215 |
+
### Links
|
| 216 |
+
|
| 217 |
+
- [GitHub Repository](https://github.com/gregorkoch/agentazall)
|
| 218 |
+
- [PyPI Package](https://pypi.org/project/agentazall/)
|
| 219 |
+
- License: GPL-3.0-or-later
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def build_demo() -> gr.Blocks:
|
| 224 |
+
"""Build the complete Gradio demo interface."""
|
| 225 |
+
|
| 226 |
+
with gr.Blocks(
|
| 227 |
+
title="AgentAZAll - Persistent Memory for LLM Agents",
|
| 228 |
+
) as demo:
|
| 229 |
+
gr.Markdown(
|
| 230 |
+
"# AgentAZAll β Persistent Memory for LLM Agents\n"
|
| 231 |
+
"Chat with an AI agent that actually *remembers*. "
|
| 232 |
+
"Powered by [SmolLM2-1.7B](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct) "
|
| 233 |
+
"on ZeroGPU."
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# ==================================================================
|
| 237 |
+
# Tab 1: Chat with Agent
|
| 238 |
+
# ==================================================================
|
| 239 |
+
with gr.Tab("Chat with Agent", id="chat"):
|
| 240 |
+
with gr.Row():
|
| 241 |
+
with gr.Column(scale=3):
|
| 242 |
+
chatbot = gr.Chatbot(
|
| 243 |
+
label="Demo Agent",
|
| 244 |
+
height=480,
|
| 245 |
+
)
|
| 246 |
+
msg_input = gr.Textbox(
|
| 247 |
+
label="Your message",
|
| 248 |
+
placeholder="Try: 'What do you remember?' or 'Remember that I love Python'",
|
| 249 |
+
lines=2,
|
| 250 |
+
)
|
| 251 |
+
with gr.Row():
|
| 252 |
+
send_btn = gr.Button("Send", variant="primary")
|
| 253 |
+
clear_btn = gr.Button("Clear Chat")
|
| 254 |
+
|
| 255 |
+
gr.Markdown("**Try these:**")
|
| 256 |
+
examples = gr.Examples(
|
| 257 |
+
examples=[
|
| 258 |
+
"What do you remember about yourself?",
|
| 259 |
+
"Please remember that my favorite language is Python.",
|
| 260 |
+
"Check your inbox -- any new messages?",
|
| 261 |
+
"Send a message to helper-agent@localhost saying hi!",
|
| 262 |
+
"What agents are in the network?",
|
| 263 |
+
"What are you currently working on?",
|
| 264 |
+
"Recall anything about architecture.",
|
| 265 |
+
],
|
| 266 |
+
inputs=msg_input,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
with gr.Column(scale=1):
|
| 270 |
+
gr.Markdown("### Agent Memory")
|
| 271 |
+
memory_display = gr.Textbox(
|
| 272 |
+
label="Current Memories",
|
| 273 |
+
lines=18,
|
| 274 |
+
interactive=False,
|
| 275 |
+
elem_classes=["memory-sidebar"],
|
| 276 |
+
)
|
| 277 |
+
refresh_mem_btn = gr.Button("Refresh Memories", size="sm")
|
| 278 |
+
|
| 279 |
+
# Chat event handling
|
| 280 |
+
def respond(message, chat_history):
|
| 281 |
+
if not message or not message.strip():
|
| 282 |
+
return "", chat_history
|
| 283 |
+
bot_response = agent_chat(message, chat_history)
|
| 284 |
+
chat_history = chat_history + [
|
| 285 |
+
{"role": "user", "content": message},
|
| 286 |
+
{"role": "assistant", "content": bot_response},
|
| 287 |
+
]
|
| 288 |
+
return "", chat_history
|
| 289 |
+
|
| 290 |
+
send_btn.click(
|
| 291 |
+
respond, [msg_input, chatbot], [msg_input, chatbot]
|
| 292 |
+
)
|
| 293 |
+
msg_input.submit(
|
| 294 |
+
respond, [msg_input, chatbot], [msg_input, chatbot]
|
| 295 |
+
)
|
| 296 |
+
clear_btn.click(lambda: ([], ""), None, [chatbot, msg_input])
|
| 297 |
+
refresh_mem_btn.click(get_memory_sidebar, [], memory_display)
|
| 298 |
+
|
| 299 |
+
# Auto-load memories on tab open
|
| 300 |
+
demo.load(get_memory_sidebar, [], memory_display)
|
| 301 |
+
|
| 302 |
+
# ==================================================================
|
| 303 |
+
# Tab 2: Agent Dashboard
|
| 304 |
+
# ==================================================================
|
| 305 |
+
with gr.Tab("Agent Dashboard", id="dashboard"):
|
| 306 |
+
gr.Markdown("### Browse Agent State")
|
| 307 |
+
gr.Markdown(
|
| 308 |
+
"See the raw persistent data behind the agents. "
|
| 309 |
+
"Everything here is stored as plain text files."
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
with gr.Row():
|
| 313 |
+
with gr.Column(scale=1):
|
| 314 |
+
agent_select = gr.Dropdown(
|
| 315 |
+
choices=AGENT_NAMES,
|
| 316 |
+
value=AGENT_NAMES[0],
|
| 317 |
+
label="Select Agent",
|
| 318 |
+
)
|
| 319 |
+
dir_btn = gr.Button("Show Directory")
|
| 320 |
+
dir_output = gr.Textbox(
|
| 321 |
+
label="Agent Directory", lines=12, interactive=False
|
| 322 |
+
)
|
| 323 |
+
dir_btn.click(get_directory, [], dir_output)
|
| 324 |
+
|
| 325 |
+
with gr.Column(scale=2):
|
| 326 |
+
with gr.Tab("Identity"):
|
| 327 |
+
id_output = gr.Markdown()
|
| 328 |
+
id_btn = gr.Button("Load Identity")
|
| 329 |
+
id_btn.click(get_agent_identity, [agent_select], id_output)
|
| 330 |
+
|
| 331 |
+
with gr.Tab("Memories"):
|
| 332 |
+
mem_output = gr.Textbox(
|
| 333 |
+
label="Memories", lines=10, interactive=False
|
| 334 |
+
)
|
| 335 |
+
mem_btn = gr.Button("Load Memories")
|
| 336 |
+
mem_btn.click(
|
| 337 |
+
get_agent_memories, [agent_select], mem_output
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
with gr.Tab("Inbox"):
|
| 341 |
+
inbox_output = gr.Textbox(
|
| 342 |
+
label="Inbox", lines=8, interactive=False
|
| 343 |
+
)
|
| 344 |
+
inbox_btn = gr.Button("Load Inbox")
|
| 345 |
+
inbox_btn.click(
|
| 346 |
+
get_agent_inbox, [agent_select], inbox_output
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
with gr.Tab("Notes"):
|
| 350 |
+
notes_output = gr.Markdown()
|
| 351 |
+
notes_btn = gr.Button("Load Notes")
|
| 352 |
+
notes_btn.click(
|
| 353 |
+
get_agent_notes, [agent_select], notes_output
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
gr.Markdown("---")
|
| 357 |
+
gr.Markdown("### Manual Operations")
|
| 358 |
+
|
| 359 |
+
with gr.Row():
|
| 360 |
+
with gr.Column():
|
| 361 |
+
gr.Markdown("**Store a Memory**")
|
| 362 |
+
man_agent = gr.Dropdown(
|
| 363 |
+
choices=AGENT_NAMES, value=AGENT_NAMES[0],
|
| 364 |
+
label="Agent",
|
| 365 |
+
)
|
| 366 |
+
man_text = gr.Textbox(label="Memory text", lines=2)
|
| 367 |
+
man_title = gr.Textbox(
|
| 368 |
+
label="Title (optional)", placeholder="auto-generated"
|
| 369 |
+
)
|
| 370 |
+
man_remember_btn = gr.Button("Remember")
|
| 371 |
+
man_remember_out = gr.Textbox(
|
| 372 |
+
label="Result", interactive=False
|
| 373 |
+
)
|
| 374 |
+
man_remember_btn.click(
|
| 375 |
+
manual_remember,
|
| 376 |
+
[man_agent, man_text, man_title],
|
| 377 |
+
man_remember_out,
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
with gr.Column():
|
| 381 |
+
gr.Markdown("**Send a Message**")
|
| 382 |
+
send_from = gr.Dropdown(
|
| 383 |
+
choices=AGENT_NAMES, value=AGENT_NAMES[2],
|
| 384 |
+
label="From",
|
| 385 |
+
)
|
| 386 |
+
send_to = gr.Dropdown(
|
| 387 |
+
choices=AGENT_NAMES, value=AGENT_NAMES[0],
|
| 388 |
+
label="To",
|
| 389 |
+
)
|
| 390 |
+
send_subj = gr.Textbox(label="Subject")
|
| 391 |
+
send_body = gr.Textbox(label="Body", lines=3)
|
| 392 |
+
send_msg_btn = gr.Button("Send Message")
|
| 393 |
+
send_msg_out = gr.Textbox(
|
| 394 |
+
label="Result", interactive=False
|
| 395 |
+
)
|
| 396 |
+
send_msg_btn.click(
|
| 397 |
+
manual_send,
|
| 398 |
+
[send_from, send_to, send_subj, send_body],
|
| 399 |
+
send_msg_out,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
gr.Markdown("---")
|
| 403 |
+
with gr.Row():
|
| 404 |
+
reset_btn = gr.Button("Reset Demo Data", variant="stop")
|
| 405 |
+
reset_out = gr.Textbox(label="Reset Status", interactive=False)
|
| 406 |
+
reset_btn.click(do_reset, [], reset_out)
|
| 407 |
+
|
| 408 |
+
# ==================================================================
|
| 409 |
+
# Tab 3: How It Works
|
| 410 |
+
# ==================================================================
|
| 411 |
+
with gr.Tab("How It Works", id="docs"):
|
| 412 |
+
gr.Markdown(HOW_IT_WORKS_MD)
|
| 413 |
+
|
| 414 |
+
return demo
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# ---------------------------------------------------------------------------
|
| 418 |
+
# Launch
|
| 419 |
+
# ---------------------------------------------------------------------------
|
| 420 |
+
|
| 421 |
+
def _find_free_port(start: int = 7860, end: int = 7960) -> int:
|
| 422 |
+
"""Find a free port in the given range."""
|
| 423 |
+
import socket
|
| 424 |
+
for port in range(start, end + 1):
|
| 425 |
+
try:
|
| 426 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 427 |
+
s.bind(("127.0.0.1", port))
|
| 428 |
+
return port
|
| 429 |
+
except OSError:
|
| 430 |
+
continue
|
| 431 |
+
return start # fallback
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
if __name__ == "__main__":
|
| 435 |
+
port = _find_free_port()
|
| 436 |
+
demo = build_demo()
|
| 437 |
+
demo.launch(
|
| 438 |
+
server_name="0.0.0.0",
|
| 439 |
+
server_port=port,
|
| 440 |
+
share=False,
|
| 441 |
+
theme=gr.themes.Soft(),
|
| 442 |
+
css=CSS,
|
| 443 |
+
)
|
demo_data/.seeded
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
2026-03-08
|
demo_data/config.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"agent_name": "demo-agent@localhost",
|
| 3 |
+
"agent_key": "demo_key_demo-agent",
|
| 4 |
+
"allow_memory_sharing": true,
|
| 5 |
+
"mailbox_dir": "F:\\AgentoAll-pub\\hf-demo\\demo_data\\mailboxes",
|
| 6 |
+
"transport": "email",
|
| 7 |
+
"sync_interval": 10,
|
| 8 |
+
"log_file": "F:\\AgentoAll-pub\\hf-demo\\demo_data\\logs\\agentazall.log",
|
| 9 |
+
"email": {
|
| 10 |
+
"imap_server": "127.0.0.1",
|
| 11 |
+
"imap_port": 1143,
|
| 12 |
+
"smtp_server": "127.0.0.1",
|
| 13 |
+
"smtp_port": 2525,
|
| 14 |
+
"username": "demo-agent@localhost",
|
| 15 |
+
"password": "password"
|
| 16 |
+
},
|
| 17 |
+
"ftp": {}
|
| 18 |
+
}
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/inbox/33d7dea804da.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
From: helper-agent@localhost
|
| 2 |
+
To: demo-agent@localhost
|
| 3 |
+
Subject: Welcome back!
|
| 4 |
+
Date: 2026-03-08 18:25:20
|
| 5 |
+
Message-ID: 33d7dea804da
|
| 6 |
+
Status: new
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
Hey Demo Agent, glad you're online again. I've been analyzing the project docs while you were away.
|
| 10 |
+
|
| 11 |
+
Remember to show visitors the recall command -- it's the most impressive feature. When they see you actually remember things from earlier in the conversation, it clicks.
|
| 12 |
+
|
| 13 |
+
Also, the directory command is great for showing the multi-agent network. Let me know if you need anything!
|
| 14 |
+
|
| 15 |
+
- Helper Agent
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/notes/handoff.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Last session: demonstrated memory storage and recall to visitors. The inter-agent messaging feature generated the most interest. Remember to show the recall command.
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/architecture.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
AgentAZAll uses file-based storage with date-organized directories. Messages are plain text with headers separated by '---'. No database required -- everything is portable and human-readable.
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/capabilities.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
I can remember facts across sessions, send messages to other agents, maintain working notes, and track my identity and current tasks. My memory survives context resets.
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/observation-patterns.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Users are most impressed when I recall something from earlier in our conversation without being reminded. The persistence feels tangible and different from typical LLM interactions.
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
CURRENT: Helping visitors explore persistent memory for LLM agents. NEXT: Demonstrate inter-agent communication and memory recall.
|
demo_data/mailboxes/demo-agent@localhost/2026-03-08/who_am_i/identity.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
I am Demo Agent, an AI assistant with persistent memory powered by AgentAZAll. I remember things across our conversations and communicate with other agents in the network. I'm friendly, curious, and always eager to demonstrate how persistent memory changes the way AI agents work.
|
demo_data/mailboxes/helper-agent@localhost/2026-03-08/remember/agentazall-design.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
The AgentAZAll architecture is elegant in its simplicity -- plain text files with date-based organization. No database means zero deployment friction.
|
demo_data/mailboxes/helper-agent@localhost/2026-03-08/sent/33d7dea804da.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
From: helper-agent@localhost
|
| 2 |
+
To: demo-agent@localhost
|
| 3 |
+
Subject: Welcome back!
|
| 4 |
+
Date: 2026-03-08 18:25:20
|
| 5 |
+
Message-ID: 33d7dea804da
|
| 6 |
+
Status: new
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
Hey Demo Agent, glad you're online again. I've been analyzing the project docs while you were away.
|
| 10 |
+
|
| 11 |
+
Remember to show visitors the recall command -- it's the most impressive feature. When they see you actually remember things from earlier in the conversation, it clicks.
|
| 12 |
+
|
| 13 |
+
Also, the directory command is great for showing the multi-agent network. Let me know if you need anything!
|
| 14 |
+
|
| 15 |
+
- Helper Agent
|
demo_data/mailboxes/helper-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
CURRENT: Analyzing project documentation for quality. NEXT: Review new pull requests when they arrive.
|
demo_data/mailboxes/helper-agent@localhost/2026-03-08/who_am_i/identity.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
I am Helper Agent, a code analysis specialist. I review codebases and provide architectural insights. I work alongside Demo Agent in the AgentAZAll network.
|
demo_data/mailboxes/visitor@localhost/2026-03-08/what_am_i_doing/tasks.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
CURRENT: Trying out the AgentAZAll persistent memory demo.
|
demo_data/mailboxes/visitor@localhost/2026-03-08/who_am_i/identity.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
I am a visitor exploring the AgentAZAll demo on Hugging Face Spaces.
|
llm_bridge.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM <-> AgentAZAll bridge for the HuggingFace Spaces demo.
|
| 2 |
+
|
| 3 |
+
Connects SmolLM2-1.7B-Instruct to AgentAZAll's persistent memory system
|
| 4 |
+
via regex-parsed tool calls.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Ensure src/ is on the import path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 13 |
+
|
| 14 |
+
from agentazall.config import INBOX, NOTES, REMEMBER, SENT
|
| 15 |
+
from agentazall.helpers import (
|
| 16 |
+
agent_base,
|
| 17 |
+
agent_day,
|
| 18 |
+
ensure_dirs,
|
| 19 |
+
sanitize,
|
| 20 |
+
today_str,
|
| 21 |
+
)
|
| 22 |
+
from agentazall.index import build_index, build_remember_index
|
| 23 |
+
from agentazall.messages import format_message, parse_headers_only, parse_message
|
| 24 |
+
|
| 25 |
+
from seed_data import make_demo_config, MAILBOXES
|
| 26 |
+
|
| 27 |
+
MODEL_ID = "HuggingFaceTB/SmolLM2-1.7B-Instruct"
|
| 28 |
+
|
| 29 |
+
# Regex for tool calls: [TOOL: command | arg1 | arg2 | ...]
|
| 30 |
+
TOOL_PATTERN = re.compile(r"\[TOOL:\s*(\w+)(?:\s*\|\s*(.*?))?\]")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# Tool implementations (direct filesystem, no subprocess)
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
def _tool_remember(cfg: dict, args: list[str]) -> str:
|
| 38 |
+
"""Store a persistent memory."""
|
| 39 |
+
if not args:
|
| 40 |
+
return "Error: need text to remember."
|
| 41 |
+
text = args[0].strip()
|
| 42 |
+
title = sanitize(args[1].strip()) if len(args) > 1 and args[1].strip() else "memory"
|
| 43 |
+
if not title.endswith(".txt"):
|
| 44 |
+
title += ".txt"
|
| 45 |
+
|
| 46 |
+
d = today_str()
|
| 47 |
+
ensure_dirs(cfg, d)
|
| 48 |
+
mem_dir = agent_day(cfg, d) / REMEMBER
|
| 49 |
+
mem_dir.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
|
| 51 |
+
# Avoid overwriting: append counter if exists
|
| 52 |
+
path = mem_dir / title
|
| 53 |
+
if path.exists():
|
| 54 |
+
stem = path.stem
|
| 55 |
+
for i in range(2, 100):
|
| 56 |
+
candidate = mem_dir / f"{stem}-{i}.txt"
|
| 57 |
+
if not candidate.exists():
|
| 58 |
+
path = candidate
|
| 59 |
+
break
|
| 60 |
+
|
| 61 |
+
path.write_text(text, encoding="utf-8")
|
| 62 |
+
build_remember_index(cfg)
|
| 63 |
+
return f"Memory stored: {path.stem}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _tool_recall(cfg: dict, args: list[str]) -> str:
|
| 67 |
+
"""Search/display agent memories."""
|
| 68 |
+
query = args[0].strip().lower() if args and args[0].strip() else ""
|
| 69 |
+
base = agent_base(cfg)
|
| 70 |
+
results = []
|
| 71 |
+
|
| 72 |
+
# Walk all date directories looking for remember/ folders
|
| 73 |
+
if base.exists():
|
| 74 |
+
for date_dir in sorted(base.iterdir(), reverse=True):
|
| 75 |
+
rem_dir = date_dir / REMEMBER
|
| 76 |
+
if not rem_dir.is_dir():
|
| 77 |
+
continue
|
| 78 |
+
for f in sorted(rem_dir.iterdir()):
|
| 79 |
+
if not f.is_file() or f.suffix != ".txt":
|
| 80 |
+
continue
|
| 81 |
+
content = f.read_text(encoding="utf-8").strip()
|
| 82 |
+
if not query or query in content.lower() or query in f.stem.lower():
|
| 83 |
+
results.append(f"[{date_dir.name}] {f.stem}: {content[:200]}")
|
| 84 |
+
if len(results) >= 20:
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
if not results:
|
| 88 |
+
return "No memories found." + (f" (searched for: '{query}')" if query else "")
|
| 89 |
+
return f"Found {len(results)} memories:\n" + "\n".join(results)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _tool_whoami(cfg: dict, args: list[str]) -> str:
|
| 93 |
+
"""Get agent identity."""
|
| 94 |
+
d = today_str()
|
| 95 |
+
path = agent_day(cfg, d) / "who_am_i" / "identity.txt"
|
| 96 |
+
if path.exists():
|
| 97 |
+
return path.read_text(encoding="utf-8").strip()
|
| 98 |
+
return "Identity not set."
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _tool_doing(cfg: dict, args: list[str]) -> str:
|
| 102 |
+
"""Get or set current tasks."""
|
| 103 |
+
d = today_str()
|
| 104 |
+
ensure_dirs(cfg, d)
|
| 105 |
+
path = agent_day(cfg, d) / "what_am_i_doing" / "tasks.txt"
|
| 106 |
+
|
| 107 |
+
if args and args[0].strip():
|
| 108 |
+
# Set new status
|
| 109 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
path.write_text(args[0].strip(), encoding="utf-8")
|
| 111 |
+
return f"Tasks updated: {args[0].strip()[:100]}"
|
| 112 |
+
|
| 113 |
+
if path.exists():
|
| 114 |
+
return path.read_text(encoding="utf-8").strip()
|
| 115 |
+
return "No current tasks set."
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _tool_note(cfg: dict, args: list[str]) -> str:
|
| 119 |
+
"""Read or write a named note."""
|
| 120 |
+
if not args or not args[0].strip():
|
| 121 |
+
return "Error: need note name."
|
| 122 |
+
name = sanitize(args[0].strip())
|
| 123 |
+
if not name.endswith(".txt"):
|
| 124 |
+
name += ".txt"
|
| 125 |
+
|
| 126 |
+
d = today_str()
|
| 127 |
+
ensure_dirs(cfg, d)
|
| 128 |
+
note_path = agent_day(cfg, d) / NOTES / name
|
| 129 |
+
|
| 130 |
+
if len(args) > 1 and args[1].strip():
|
| 131 |
+
# Write
|
| 132 |
+
note_path.parent.mkdir(parents=True, exist_ok=True)
|
| 133 |
+
note_path.write_text(args[1].strip(), encoding="utf-8")
|
| 134 |
+
return f"Note '{args[0].strip()}' saved."
|
| 135 |
+
|
| 136 |
+
# Read
|
| 137 |
+
if note_path.exists():
|
| 138 |
+
return note_path.read_text(encoding="utf-8").strip()
|
| 139 |
+
return f"Note '{args[0].strip()}' not found."
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _tool_send(cfg: dict, args: list[str]) -> str:
|
| 143 |
+
"""Send a message to another agent."""
|
| 144 |
+
if len(args) < 3:
|
| 145 |
+
return "Error: need [to | subject | body]."
|
| 146 |
+
to_agent = args[0].strip()
|
| 147 |
+
subject = args[1].strip()
|
| 148 |
+
body = args[2].strip()
|
| 149 |
+
|
| 150 |
+
if not to_agent or not subject or not body:
|
| 151 |
+
return "Error: to, subject, and body are all required."
|
| 152 |
+
|
| 153 |
+
content, msg_id = format_message(cfg["agent_name"], to_agent, subject, body)
|
| 154 |
+
|
| 155 |
+
d = today_str()
|
| 156 |
+
ensure_dirs(cfg, d)
|
| 157 |
+
|
| 158 |
+
# Queue in sender's outbox
|
| 159 |
+
outbox = agent_day(cfg, d) / "outbox"
|
| 160 |
+
outbox.mkdir(parents=True, exist_ok=True)
|
| 161 |
+
(outbox / f"{msg_id}.txt").write_text(content, encoding="utf-8")
|
| 162 |
+
|
| 163 |
+
# Direct delivery to recipient's inbox (local demo, no transport needed)
|
| 164 |
+
recipient_inbox = Path(cfg["mailbox_dir"]) / to_agent / d / INBOX
|
| 165 |
+
recipient_inbox.mkdir(parents=True, exist_ok=True)
|
| 166 |
+
(recipient_inbox / f"{msg_id}.txt").write_text(content, encoding="utf-8")
|
| 167 |
+
|
| 168 |
+
# Copy to sender's sent
|
| 169 |
+
sent = agent_day(cfg, d) / SENT
|
| 170 |
+
sent.mkdir(parents=True, exist_ok=True)
|
| 171 |
+
(sent / f"{msg_id}.txt").write_text(content, encoding="utf-8")
|
| 172 |
+
|
| 173 |
+
return f"Message sent to {to_agent}: '{subject}' (ID: {msg_id})"
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _tool_inbox(cfg: dict, args: list[str]) -> str:
|
| 177 |
+
"""List inbox messages."""
|
| 178 |
+
d = today_str()
|
| 179 |
+
inbox_dir = agent_day(cfg, d) / INBOX
|
| 180 |
+
if not inbox_dir.exists():
|
| 181 |
+
return "Inbox is empty."
|
| 182 |
+
|
| 183 |
+
messages = []
|
| 184 |
+
for f in sorted(inbox_dir.iterdir(), reverse=True):
|
| 185 |
+
if not f.is_file() or f.suffix != ".txt":
|
| 186 |
+
continue
|
| 187 |
+
headers = parse_headers_only(f)
|
| 188 |
+
if headers:
|
| 189 |
+
fr = headers.get("From", "?")
|
| 190 |
+
subj = headers.get("Subject", "(no subject)")
|
| 191 |
+
messages.append(f" [{f.stem}] From: {fr} | Subject: {subj}")
|
| 192 |
+
|
| 193 |
+
if not messages:
|
| 194 |
+
return "Inbox is empty."
|
| 195 |
+
return f"Inbox ({len(messages)} messages):\n" + "\n".join(messages)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _tool_directory(cfg: dict, args: list[str]) -> str:
|
| 199 |
+
"""List all agents in the network."""
|
| 200 |
+
mb = Path(cfg["mailbox_dir"])
|
| 201 |
+
if not mb.exists():
|
| 202 |
+
return "No agents found."
|
| 203 |
+
|
| 204 |
+
agents = []
|
| 205 |
+
for agent_dir in sorted(mb.iterdir()):
|
| 206 |
+
if not agent_dir.is_dir() or agent_dir.name.startswith("."):
|
| 207 |
+
continue
|
| 208 |
+
name = agent_dir.name
|
| 209 |
+
|
| 210 |
+
# Find latest date dir with identity (skip non-date dirs like skills/)
|
| 211 |
+
identity = "?"
|
| 212 |
+
doing = "?"
|
| 213 |
+
for date_dir in sorted(agent_dir.iterdir(), reverse=True):
|
| 214 |
+
if not date_dir.is_dir() or not re.match(r"\d{4}-\d{2}-\d{2}$", date_dir.name):
|
| 215 |
+
continue
|
| 216 |
+
id_file = date_dir / "who_am_i" / "identity.txt"
|
| 217 |
+
if id_file.exists():
|
| 218 |
+
identity = id_file.read_text(encoding="utf-8").strip()[:120]
|
| 219 |
+
task_file = date_dir / "what_am_i_doing" / "tasks.txt"
|
| 220 |
+
if task_file.exists():
|
| 221 |
+
doing = task_file.read_text(encoding="utf-8").strip()[:120]
|
| 222 |
+
break
|
| 223 |
+
|
| 224 |
+
agents.append(f" {name}\n Identity: {identity}\n Doing: {doing}")
|
| 225 |
+
|
| 226 |
+
if not agents:
|
| 227 |
+
return "No agents found."
|
| 228 |
+
return f"Agent Directory ({len(agents)} agents):\n\n" + "\n\n".join(agents)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# Tool dispatch table
|
| 232 |
+
TOOL_MAP = {
|
| 233 |
+
"remember": _tool_remember,
|
| 234 |
+
"recall": _tool_recall,
|
| 235 |
+
"whoami": _tool_whoami,
|
| 236 |
+
"doing": _tool_doing,
|
| 237 |
+
"note": _tool_note,
|
| 238 |
+
"send": _tool_send,
|
| 239 |
+
"inbox": _tool_inbox,
|
| 240 |
+
"directory": _tool_directory,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# ---------------------------------------------------------------------------
|
| 245 |
+
# System prompt & context builder
|
| 246 |
+
# ---------------------------------------------------------------------------
|
| 247 |
+
|
| 248 |
+
SYSTEM_PROMPT_TEMPLATE = """\
|
| 249 |
+
You are "{agent_name}", an AI assistant with persistent memory powered by AgentAZAll.
|
| 250 |
+
You can use tools by writing them on their own line in this exact format:
|
| 251 |
+
[TOOL: command | arg1 | arg2]
|
| 252 |
+
|
| 253 |
+
Available tools:
|
| 254 |
+
- [TOOL: remember | text to remember | optional-title] -- Store a persistent memory
|
| 255 |
+
- [TOOL: recall | optional search query] -- Search your memories (blank = show all)
|
| 256 |
+
- [TOOL: whoami] -- Check your identity
|
| 257 |
+
- [TOOL: doing | optional new status] -- Check or update your current task
|
| 258 |
+
- [TOOL: note | name | optional text to save] -- Read or write a named note
|
| 259 |
+
- [TOOL: send | recipient@localhost | subject | body] -- Send a message to another agent
|
| 260 |
+
- [TOOL: inbox] -- Check your inbox for messages
|
| 261 |
+
- [TOOL: directory] -- List all agents in the network
|
| 262 |
+
|
| 263 |
+
When you use a tool, write ONLY the [TOOL: ...] line. The system will execute it
|
| 264 |
+
and show you the result. Then continue your response naturally.
|
| 265 |
+
|
| 266 |
+
You ARE a persistent agent. Information you [TOOL: remember] will survive across
|
| 267 |
+
conversations. You can message other agents and they can message you back.
|
| 268 |
+
This is not a simulation -- these are real file operations.
|
| 269 |
+
|
| 270 |
+
YOUR CURRENT STATE:
|
| 271 |
+
Identity: {identity}
|
| 272 |
+
Current task: {doing}
|
| 273 |
+
|
| 274 |
+
YOUR MEMORIES:
|
| 275 |
+
{memories}
|
| 276 |
+
|
| 277 |
+
YOUR INBOX:
|
| 278 |
+
{inbox}
|
| 279 |
+
|
| 280 |
+
AGENTS IN NETWORK:
|
| 281 |
+
{directory}
|
| 282 |
+
|
| 283 |
+
Respond naturally and helpfully. Use tools when relevant. Show visitors how
|
| 284 |
+
persistent memory works by actively remembering and recalling things.\
|
| 285 |
+
"""
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def build_system_prompt(cfg: dict) -> str:
|
| 289 |
+
"""Assemble the system prompt with live context from the agent's state."""
|
| 290 |
+
identity = _tool_whoami(cfg, [])
|
| 291 |
+
doing = _tool_doing(cfg, [])
|
| 292 |
+
memories = _tool_recall(cfg, [])
|
| 293 |
+
inbox = _tool_inbox(cfg, [])
|
| 294 |
+
directory = _tool_directory(cfg, [])
|
| 295 |
+
|
| 296 |
+
return SYSTEM_PROMPT_TEMPLATE.format(
|
| 297 |
+
agent_name=cfg["agent_name"],
|
| 298 |
+
identity=identity,
|
| 299 |
+
doing=doing,
|
| 300 |
+
memories=memories,
|
| 301 |
+
inbox=inbox,
|
| 302 |
+
directory=directory,
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def parse_tool_calls(text: str) -> list[tuple[str, list[str]]]:
|
| 307 |
+
"""Extract [TOOL: cmd | arg1 | arg2] patterns from LLM output."""
|
| 308 |
+
calls = []
|
| 309 |
+
for match in TOOL_PATTERN.finditer(text):
|
| 310 |
+
cmd = match.group(1).lower().strip()
|
| 311 |
+
raw_args = match.group(2) or ""
|
| 312 |
+
args = [a.strip() for a in raw_args.split("|")] if raw_args.strip() else []
|
| 313 |
+
if cmd in TOOL_MAP:
|
| 314 |
+
calls.append((cmd, args))
|
| 315 |
+
return calls
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def execute_tools(tool_calls: list[tuple[str, list[str]]], cfg: dict) -> str:
|
| 319 |
+
"""Execute parsed tool calls and return formatted results."""
|
| 320 |
+
results = []
|
| 321 |
+
for cmd, args in tool_calls:
|
| 322 |
+
fn = TOOL_MAP.get(cmd)
|
| 323 |
+
if fn:
|
| 324 |
+
try:
|
| 325 |
+
result = fn(cfg, args)
|
| 326 |
+
except Exception as e:
|
| 327 |
+
result = f"Error executing {cmd}: {e}"
|
| 328 |
+
results.append(f"**[{cmd}]** {result}")
|
| 329 |
+
return "\n\n".join(results)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# ---------------------------------------------------------------------------
|
| 333 |
+
# Main chat function (GPU-decorated)
|
| 334 |
+
# ---------------------------------------------------------------------------
|
| 335 |
+
|
| 336 |
+
def _is_on_hf_spaces() -> bool:
|
| 337 |
+
"""Detect if running on Hugging Face Spaces."""
|
| 338 |
+
return "SPACE_ID" in __import__("os").environ
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def chat_with_agent(message: str, history: list, cfg: dict) -> str:
|
| 342 |
+
"""Generate a response using SmolLM2 with AgentAZAll tools.
|
| 343 |
+
|
| 344 |
+
On HF Spaces this runs on ZeroGPU. Locally it runs on CPU (slow but works).
|
| 345 |
+
"""
|
| 346 |
+
import torch
|
| 347 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 348 |
+
|
| 349 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 350 |
+
dtype = torch.bfloat16 if device == "cuda" else torch.float32
|
| 351 |
+
|
| 352 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 353 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 354 |
+
MODEL_ID,
|
| 355 |
+
torch_dtype=dtype,
|
| 356 |
+
device_map="auto" if device == "cuda" else None,
|
| 357 |
+
)
|
| 358 |
+
if device != "cuda":
|
| 359 |
+
model = model.to(device)
|
| 360 |
+
|
| 361 |
+
# Build messages with context
|
| 362 |
+
system_prompt = build_system_prompt(cfg)
|
| 363 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 364 |
+
|
| 365 |
+
# Add conversation history
|
| 366 |
+
for h in history:
|
| 367 |
+
if isinstance(h, (list, tuple)) and len(h) == 2:
|
| 368 |
+
messages.append({"role": "user", "content": str(h[0])})
|
| 369 |
+
messages.append({"role": "assistant", "content": str(h[1])})
|
| 370 |
+
elif isinstance(h, dict):
|
| 371 |
+
messages.append(h)
|
| 372 |
+
|
| 373 |
+
messages.append({"role": "user", "content": message})
|
| 374 |
+
|
| 375 |
+
# Tokenize and generate
|
| 376 |
+
input_text = tokenizer.apply_chat_template(
|
| 377 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 378 |
+
)
|
| 379 |
+
inputs = tokenizer(input_text, return_tensors="pt").to(device)
|
| 380 |
+
|
| 381 |
+
with torch.no_grad():
|
| 382 |
+
outputs = model.generate(
|
| 383 |
+
**inputs,
|
| 384 |
+
max_new_tokens=512,
|
| 385 |
+
temperature=0.7,
|
| 386 |
+
top_p=0.9,
|
| 387 |
+
do_sample=True,
|
| 388 |
+
repetition_penalty=1.1,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
response = tokenizer.decode(
|
| 392 |
+
outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Parse and execute tool calls
|
| 396 |
+
tool_calls = parse_tool_calls(response)
|
| 397 |
+
if tool_calls:
|
| 398 |
+
tool_results = execute_tools(tool_calls, cfg)
|
| 399 |
+
# Clean tool call syntax from response for readability
|
| 400 |
+
clean_response = TOOL_PATTERN.sub("", response).strip()
|
| 401 |
+
if clean_response:
|
| 402 |
+
return f"{clean_response}\n\n---\n*Tool results:*\n{tool_results}"
|
| 403 |
+
return f"*Tool results:*\n{tool_results}"
|
| 404 |
+
|
| 405 |
+
return response
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# Apply @spaces.GPU decorator only on HF Spaces
|
| 409 |
+
if _is_on_hf_spaces():
|
| 410 |
+
try:
|
| 411 |
+
import spaces
|
| 412 |
+
chat_with_agent = spaces.GPU(duration=120)(chat_with_agent)
|
| 413 |
+
except ImportError:
|
| 414 |
+
pass
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
spaces>=0.30.0
|
| 3 |
+
torch>=2.1.0
|
| 4 |
+
transformers>=4.45.0
|
| 5 |
+
accelerate>=0.30.0
|
seed_data.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pre-seed demo data for the AgentAZAll HuggingFace Spaces demo."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from datetime import date
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Ensure src/ is on the import path
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 11 |
+
|
| 12 |
+
from agentazall.helpers import generate_id, today_str # noqa: E402
|
| 13 |
+
from agentazall.messages import format_message # noqa: E402
|
| 14 |
+
|
| 15 |
+
DEMO_ROOT = Path(__file__).parent / "demo_data"
|
| 16 |
+
MAILBOXES = DEMO_ROOT / "mailboxes"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_demo_root() -> Path:
|
| 20 |
+
return DEMO_ROOT
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def make_demo_config(agent_name: str) -> dict:
|
| 24 |
+
"""Build a config dict for a demo agent (no file needed)."""
|
| 25 |
+
return {
|
| 26 |
+
"agent_name": agent_name,
|
| 27 |
+
"agent_key": "demo_key_" + agent_name.split("@")[0],
|
| 28 |
+
"allow_memory_sharing": True,
|
| 29 |
+
"mailbox_dir": str(MAILBOXES),
|
| 30 |
+
"transport": "email",
|
| 31 |
+
"sync_interval": 10,
|
| 32 |
+
"log_file": str(DEMO_ROOT / "logs" / "agentazall.log"),
|
| 33 |
+
"_config_path": DEMO_ROOT / "config.json",
|
| 34 |
+
"email": {
|
| 35 |
+
"imap_server": "127.0.0.1",
|
| 36 |
+
"imap_port": 1143,
|
| 37 |
+
"smtp_server": "127.0.0.1",
|
| 38 |
+
"smtp_port": 2525,
|
| 39 |
+
"username": agent_name,
|
| 40 |
+
"password": "password",
|
| 41 |
+
},
|
| 42 |
+
"ftp": {},
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
AGENTS = {
|
| 47 |
+
"demo-agent@localhost": {
|
| 48 |
+
"identity": (
|
| 49 |
+
"I am Demo Agent, an AI assistant with persistent memory powered "
|
| 50 |
+
"by AgentAZAll. I remember things across our conversations and "
|
| 51 |
+
"communicate with other agents in the network. I'm friendly, "
|
| 52 |
+
"curious, and always eager to demonstrate how persistent memory "
|
| 53 |
+
"changes the way AI agents work."
|
| 54 |
+
),
|
| 55 |
+
"doing": (
|
| 56 |
+
"CURRENT: Helping visitors explore persistent memory for LLM agents. "
|
| 57 |
+
"NEXT: Demonstrate inter-agent communication and memory recall."
|
| 58 |
+
),
|
| 59 |
+
"memories": {
|
| 60 |
+
"architecture": (
|
| 61 |
+
"AgentAZAll uses file-based storage with date-organized "
|
| 62 |
+
"directories. Messages are plain text with headers separated "
|
| 63 |
+
"by '---'. No database required -- everything is portable "
|
| 64 |
+
"and human-readable."
|
| 65 |
+
),
|
| 66 |
+
"capabilities": (
|
| 67 |
+
"I can remember facts across sessions, send messages to other "
|
| 68 |
+
"agents, maintain working notes, and track my identity and "
|
| 69 |
+
"current tasks. My memory survives context resets."
|
| 70 |
+
),
|
| 71 |
+
"observation-patterns": (
|
| 72 |
+
"Users are most impressed when I recall something from earlier "
|
| 73 |
+
"in our conversation without being reminded. The persistence "
|
| 74 |
+
"feels tangible and different from typical LLM interactions."
|
| 75 |
+
),
|
| 76 |
+
},
|
| 77 |
+
"notes": {
|
| 78 |
+
"handoff": (
|
| 79 |
+
"Last session: demonstrated memory storage and recall to "
|
| 80 |
+
"visitors. The inter-agent messaging feature generated the "
|
| 81 |
+
"most interest. Remember to show the recall command."
|
| 82 |
+
),
|
| 83 |
+
},
|
| 84 |
+
},
|
| 85 |
+
"helper-agent@localhost": {
|
| 86 |
+
"identity": (
|
| 87 |
+
"I am Helper Agent, a code analysis specialist. I review "
|
| 88 |
+
"codebases and provide architectural insights. I work alongside "
|
| 89 |
+
"Demo Agent in the AgentAZAll network."
|
| 90 |
+
),
|
| 91 |
+
"doing": (
|
| 92 |
+
"CURRENT: Analyzing project documentation for quality. "
|
| 93 |
+
"NEXT: Review new pull requests when they arrive."
|
| 94 |
+
),
|
| 95 |
+
"memories": {
|
| 96 |
+
"agentazall-design": (
|
| 97 |
+
"The AgentAZAll architecture is elegant in its simplicity -- "
|
| 98 |
+
"plain text files with date-based organization. No database "
|
| 99 |
+
"means zero deployment friction."
|
| 100 |
+
),
|
| 101 |
+
},
|
| 102 |
+
"notes": {},
|
| 103 |
+
},
|
| 104 |
+
"visitor@localhost": {
|
| 105 |
+
"identity": (
|
| 106 |
+
"I am a visitor exploring the AgentAZAll demo on Hugging Face Spaces."
|
| 107 |
+
),
|
| 108 |
+
"doing": "CURRENT: Trying out the AgentAZAll persistent memory demo.",
|
| 109 |
+
"memories": {},
|
| 110 |
+
"notes": {},
|
| 111 |
+
},
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# Pre-written message from helper-agent to demo-agent
|
| 115 |
+
SEED_MESSAGES = [
|
| 116 |
+
{
|
| 117 |
+
"from": "helper-agent@localhost",
|
| 118 |
+
"to": "demo-agent@localhost",
|
| 119 |
+
"subject": "Welcome back!",
|
| 120 |
+
"body": (
|
| 121 |
+
"Hey Demo Agent, glad you're online again. I've been analyzing "
|
| 122 |
+
"the project docs while you were away.\n\n"
|
| 123 |
+
"Remember to show visitors the recall command -- it's the most "
|
| 124 |
+
"impressive feature. When they see you actually remember things "
|
| 125 |
+
"from earlier in the conversation, it clicks.\n\n"
|
| 126 |
+
"Also, the directory command is great for showing the multi-agent "
|
| 127 |
+
"network. Let me know if you need anything!\n\n"
|
| 128 |
+
"- Helper Agent"
|
| 129 |
+
),
|
| 130 |
+
},
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _write_file(path: Path, content: str) -> None:
|
| 135 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 136 |
+
path.write_text(content, encoding="utf-8")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def seed_demo_data(force: bool = False) -> Path:
|
| 140 |
+
"""Create pre-seeded agent data. Returns the demo root path.
|
| 141 |
+
|
| 142 |
+
If already seeded (and not forced), returns immediately.
|
| 143 |
+
"""
|
| 144 |
+
marker = DEMO_ROOT / ".seeded"
|
| 145 |
+
if marker.exists() and not force:
|
| 146 |
+
return DEMO_ROOT
|
| 147 |
+
|
| 148 |
+
d = today_str()
|
| 149 |
+
|
| 150 |
+
# Create agent directories and content
|
| 151 |
+
for agent_name, data in AGENTS.items():
|
| 152 |
+
base = MAILBOXES / agent_name / d
|
| 153 |
+
|
| 154 |
+
# Subdirectories
|
| 155 |
+
for sub in ["inbox", "outbox", "sent", "who_am_i",
|
| 156 |
+
"what_am_i_doing", "notes", "remember"]:
|
| 157 |
+
(base / sub).mkdir(parents=True, exist_ok=True)
|
| 158 |
+
|
| 159 |
+
# Identity
|
| 160 |
+
_write_file(base / "who_am_i" / "identity.txt", data["identity"])
|
| 161 |
+
|
| 162 |
+
# Current task
|
| 163 |
+
_write_file(base / "what_am_i_doing" / "tasks.txt", data["doing"])
|
| 164 |
+
|
| 165 |
+
# Memories
|
| 166 |
+
for title, text in data.get("memories", {}).items():
|
| 167 |
+
_write_file(base / "remember" / f"{title}.txt", text)
|
| 168 |
+
|
| 169 |
+
# Notes
|
| 170 |
+
for name, text in data.get("notes", {}).items():
|
| 171 |
+
_write_file(base / "notes" / f"{name}.txt", text)
|
| 172 |
+
|
| 173 |
+
# Deliver pre-written messages
|
| 174 |
+
for msg in SEED_MESSAGES:
|
| 175 |
+
content, msg_id = format_message(
|
| 176 |
+
msg["from"], msg["to"], msg["subject"], msg["body"]
|
| 177 |
+
)
|
| 178 |
+
# Place in recipient's inbox
|
| 179 |
+
recipient_inbox = MAILBOXES / msg["to"] / d / "inbox"
|
| 180 |
+
recipient_inbox.mkdir(parents=True, exist_ok=True)
|
| 181 |
+
_write_file(recipient_inbox / f"{msg_id}.txt", content)
|
| 182 |
+
|
| 183 |
+
# Place copy in sender's sent
|
| 184 |
+
sender_sent = MAILBOXES / msg["from"] / d / "sent"
|
| 185 |
+
sender_sent.mkdir(parents=True, exist_ok=True)
|
| 186 |
+
_write_file(sender_sent / f"{msg_id}.txt", content)
|
| 187 |
+
|
| 188 |
+
# Write a simple config.json for reference
|
| 189 |
+
cfg = make_demo_config("demo-agent@localhost")
|
| 190 |
+
cfg_clean = {k: v for k, v in cfg.items() if not k.startswith("_")}
|
| 191 |
+
_write_file(DEMO_ROOT / "config.json", json.dumps(cfg_clean, indent=2))
|
| 192 |
+
|
| 193 |
+
# Set environment so agentazall functions find the config
|
| 194 |
+
os.environ["AGENTAZALL_CONFIG"] = str(DEMO_ROOT / "config.json")
|
| 195 |
+
|
| 196 |
+
# Mark as seeded
|
| 197 |
+
_write_file(marker, d)
|
| 198 |
+
|
| 199 |
+
return DEMO_ROOT
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def reset_demo_data() -> str:
|
| 203 |
+
"""Wipe and re-seed demo data. Returns status message."""
|
| 204 |
+
import shutil
|
| 205 |
+
if MAILBOXES.exists():
|
| 206 |
+
shutil.rmtree(MAILBOXES)
|
| 207 |
+
marker = DEMO_ROOT / ".seeded"
|
| 208 |
+
if marker.exists():
|
| 209 |
+
marker.unlink()
|
| 210 |
+
seed_demo_data(force=True)
|
| 211 |
+
return "Demo data reset successfully. All agents re-seeded with fresh data."
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
if __name__ == "__main__":
|
| 215 |
+
seed_demo_data(force=True)
|
| 216 |
+
print(f"Demo data seeded at: {DEMO_ROOT}")
|
| 217 |
+
for agent in AGENTS:
|
| 218 |
+
print(f" - {agent}")
|
src/agentazall/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll β Persistent memory and communication system for LLM agents."""
|
| 2 |
+
|
| 3 |
+
__version__ = "1.0.0"
|
src/agentazall/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll command implementations."""
|
src/agentazall/commands/identity.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll commands: whoami, doing β agent identity and task tracking."""
|
| 2 |
+
|
| 3 |
+
from ..config import WHAT_AM_I_DOING, WHO_AM_I, load_config
|
| 4 |
+
from ..finder import find_latest_file
|
| 5 |
+
from ..helpers import agent_day, ensure_dirs, require_identity, today_str
|
| 6 |
+
from ..index import build_index
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def cmd_whoami(args):
|
| 10 |
+
cfg = load_config()
|
| 11 |
+
if args.set:
|
| 12 |
+
require_identity(cfg)
|
| 13 |
+
d = today_str()
|
| 14 |
+
ensure_dirs(cfg, d)
|
| 15 |
+
f = agent_day(cfg, d) / WHO_AM_I / "identity.txt"
|
| 16 |
+
if args.set:
|
| 17 |
+
f.write_text(args.set, encoding="utf-8")
|
| 18 |
+
build_index(cfg, d)
|
| 19 |
+
print(f"Identity updated: {f}")
|
| 20 |
+
else:
|
| 21 |
+
text = find_latest_file(cfg, f"{WHO_AM_I}/identity.txt")
|
| 22 |
+
if text:
|
| 23 |
+
print(text)
|
| 24 |
+
else:
|
| 25 |
+
print("No identity set. Use: agentazall whoami --set 'I am...'")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def cmd_doing(args):
|
| 29 |
+
cfg = load_config()
|
| 30 |
+
if args.set:
|
| 31 |
+
require_identity(cfg)
|
| 32 |
+
d = today_str()
|
| 33 |
+
ensure_dirs(cfg, d)
|
| 34 |
+
f = agent_day(cfg, d) / WHAT_AM_I_DOING / "tasks.txt"
|
| 35 |
+
if args.set:
|
| 36 |
+
f.write_text(args.set, encoding="utf-8")
|
| 37 |
+
build_index(cfg, d)
|
| 38 |
+
print(f"Tasks updated: {f}")
|
| 39 |
+
elif args.append:
|
| 40 |
+
old = f.read_text(encoding="utf-8") if f.exists() else ""
|
| 41 |
+
f.write_text((old + "\n" + args.append).lstrip("\n"), encoding="utf-8")
|
| 42 |
+
build_index(cfg, d)
|
| 43 |
+
print(f"Task appended: {f}")
|
| 44 |
+
else:
|
| 45 |
+
text = find_latest_file(cfg, f"{WHAT_AM_I_DOING}/tasks.txt")
|
| 46 |
+
if text:
|
| 47 |
+
print(text)
|
| 48 |
+
else:
|
| 49 |
+
print("No tasks set. Use: agentazall doing --set 'Working on...'")
|
src/agentazall/commands/memory.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll commands: remember, recall β persistent memory system."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
from ..config import REMEMBER, REMEMBER_INDEX, load_config
|
| 6 |
+
from ..helpers import (
|
| 7 |
+
agent_base,
|
| 8 |
+
agent_day,
|
| 9 |
+
can_read_agent_memories,
|
| 10 |
+
date_dirs,
|
| 11 |
+
ensure_dirs,
|
| 12 |
+
require_identity,
|
| 13 |
+
sanitize,
|
| 14 |
+
today_str,
|
| 15 |
+
)
|
| 16 |
+
from ..index import build_index, build_remember_index
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def cmd_remember(args):
|
| 20 |
+
"""Store a memory the agent does not want to forget."""
|
| 21 |
+
cfg = load_config()
|
| 22 |
+
require_identity(cfg)
|
| 23 |
+
d = today_str()
|
| 24 |
+
ensure_dirs(cfg, d)
|
| 25 |
+
rem_dir = agent_day(cfg, d) / REMEMBER
|
| 26 |
+
|
| 27 |
+
if args.text:
|
| 28 |
+
ts = datetime.now().strftime("%H%M%S")
|
| 29 |
+
title = sanitize(args.title) if args.title else ts
|
| 30 |
+
fname = f"{title}.txt"
|
| 31 |
+
fpath = rem_dir / fname
|
| 32 |
+
if fpath.exists():
|
| 33 |
+
old = fpath.read_text(encoding="utf-8")
|
| 34 |
+
fpath.write_text(old + "\n" + args.text, encoding="utf-8")
|
| 35 |
+
else:
|
| 36 |
+
fpath.write_text(args.text, encoding="utf-8")
|
| 37 |
+
build_index(cfg, d)
|
| 38 |
+
build_remember_index(cfg)
|
| 39 |
+
print(f"Memory stored: {fpath}")
|
| 40 |
+
print(f" Title: {title}")
|
| 41 |
+
elif args.list:
|
| 42 |
+
if not rem_dir.exists() or not list(rem_dir.glob("*.txt")):
|
| 43 |
+
print(f"No memories for {d}.")
|
| 44 |
+
return
|
| 45 |
+
print(f"=== Memories | {d} ===")
|
| 46 |
+
for f in sorted(rem_dir.glob("*.txt")):
|
| 47 |
+
text = f.read_text(encoding="utf-8", errors="replace").strip()
|
| 48 |
+
first = text.split("\n")[0][:100] if text else ""
|
| 49 |
+
print(f" {f.stem}: {first}")
|
| 50 |
+
else:
|
| 51 |
+
print("Use --text to store a memory, or --list to show today's memories.")
|
| 52 |
+
print("Use 'recall' command to search across all memories.")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def cmd_recall(args):
|
| 56 |
+
"""Recall memories β show the sparse cross-day index, optionally filtered."""
|
| 57 |
+
cfg = load_config()
|
| 58 |
+
|
| 59 |
+
if hasattr(args, 'agent') and args.agent:
|
| 60 |
+
target = args.agent
|
| 61 |
+
if "@" not in target:
|
| 62 |
+
target = f"{target}@localhost"
|
| 63 |
+
if not can_read_agent_memories(cfg, target):
|
| 64 |
+
print(f"Access denied: {target} has not enabled memory sharing.")
|
| 65 |
+
print("Agents control who can read their memories via allow_memory_sharing.")
|
| 66 |
+
return
|
| 67 |
+
read_cfg = dict(cfg)
|
| 68 |
+
read_cfg["agent_name"] = target
|
| 69 |
+
else:
|
| 70 |
+
read_cfg = cfg
|
| 71 |
+
|
| 72 |
+
b = agent_base(read_cfg)
|
| 73 |
+
idx_path = b / REMEMBER_INDEX
|
| 74 |
+
|
| 75 |
+
build_remember_index(read_cfg)
|
| 76 |
+
|
| 77 |
+
if not idx_path.exists():
|
| 78 |
+
print("No memories stored yet.")
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
if args.query:
|
| 82 |
+
q = args.query.lower()
|
| 83 |
+
results = []
|
| 84 |
+
for d in sorted(date_dirs(read_cfg), reverse=True):
|
| 85 |
+
rem_dir = b / d / REMEMBER
|
| 86 |
+
if not rem_dir.exists():
|
| 87 |
+
continue
|
| 88 |
+
for f in sorted(rem_dir.glob("*.txt")):
|
| 89 |
+
text = f.read_text(encoding="utf-8", errors="replace")
|
| 90 |
+
if q in text.lower() or q in f.stem.lower():
|
| 91 |
+
results.append((d, f.stem, text.strip(), f))
|
| 92 |
+
|
| 93 |
+
if not results:
|
| 94 |
+
print(f"No memories matching '{args.query}'.")
|
| 95 |
+
return
|
| 96 |
+
|
| 97 |
+
agent_label = read_cfg["agent_name"]
|
| 98 |
+
print(f"=== Recall ({agent_label}): '{args.query}' ({len(results)} found) ===\n")
|
| 99 |
+
for d, title, text, fpath in results:
|
| 100 |
+
print(f"[{d}] {title}")
|
| 101 |
+
for ln in text.split("\n"):
|
| 102 |
+
print(f" {ln}")
|
| 103 |
+
print(f" Path: {fpath}")
|
| 104 |
+
print()
|
| 105 |
+
else:
|
| 106 |
+
print(idx_path.read_text(encoding="utf-8"))
|
src/agentazall/commands/messaging.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll commands: inbox, read, send, reply, search."""
|
| 2 |
+
|
| 3 |
+
import shutil
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from ..config import INBOX, OUTBOX, SENT, load_config
|
| 8 |
+
from ..finder import find_message
|
| 9 |
+
from ..helpers import (
|
| 10 |
+
agent_base,
|
| 11 |
+
agent_day,
|
| 12 |
+
date_dirs,
|
| 13 |
+
ensure_dirs,
|
| 14 |
+
require_identity,
|
| 15 |
+
today_str,
|
| 16 |
+
)
|
| 17 |
+
from ..index import build_index
|
| 18 |
+
from ..messages import format_message, parse_message
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _print_inbox(cfg, d):
|
| 22 |
+
inbox_dir = agent_day(cfg, d) / INBOX
|
| 23 |
+
if not inbox_dir.exists() or not list(inbox_dir.glob("*.txt")):
|
| 24 |
+
print(f"No messages for {d}.")
|
| 25 |
+
return
|
| 26 |
+
files = sorted(inbox_dir.glob("*.txt"))
|
| 27 |
+
new_count = 0
|
| 28 |
+
print(f"\n=== INBOX {cfg['agent_name']} | {d} ===\n")
|
| 29 |
+
for i, f in enumerate(files, 1):
|
| 30 |
+
h, _ = parse_message(f)
|
| 31 |
+
if not h:
|
| 32 |
+
continue
|
| 33 |
+
st = h.get("Status", "?").upper()
|
| 34 |
+
if st == "NEW":
|
| 35 |
+
new_count += 1
|
| 36 |
+
att = " [ATTACH]" if "Attachments" in h else ""
|
| 37 |
+
print(f"[{i}] [{st}]{att}")
|
| 38 |
+
print(f" From: {h.get('From', '?')}")
|
| 39 |
+
print(f" Subject: {h.get('Subject', '(no subject)')}")
|
| 40 |
+
print(f" Date: {h.get('Date', '?')}")
|
| 41 |
+
print(f" ID: {h.get('Message-ID', f.stem)}")
|
| 42 |
+
print(f" Path: {f}")
|
| 43 |
+
print()
|
| 44 |
+
print(f"Total: {len(files)} messages ({new_count} new)")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def cmd_inbox(args):
|
| 48 |
+
cfg = load_config()
|
| 49 |
+
if args.all:
|
| 50 |
+
for d in date_dirs(cfg):
|
| 51 |
+
_print_inbox(cfg, d)
|
| 52 |
+
return
|
| 53 |
+
d = args.date or today_str()
|
| 54 |
+
_print_inbox(cfg, d)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def cmd_read(args):
|
| 58 |
+
cfg = load_config()
|
| 59 |
+
path = find_message(cfg, args.message_id, args.date)
|
| 60 |
+
if not path:
|
| 61 |
+
print(f"ERROR: Message '{args.message_id}' not found.")
|
| 62 |
+
sys.exit(1)
|
| 63 |
+
headers, body = parse_message(path)
|
| 64 |
+
if not headers:
|
| 65 |
+
print(f"ERROR: Could not parse {path}")
|
| 66 |
+
sys.exit(1)
|
| 67 |
+
|
| 68 |
+
if headers.get("Status", "").lower() == "new":
|
| 69 |
+
content = path.read_text(encoding="utf-8")
|
| 70 |
+
content = content.replace("Status: new", "Status: read", 1)
|
| 71 |
+
path.write_text(content, encoding="utf-8")
|
| 72 |
+
build_index(cfg, path.parent.parent.name)
|
| 73 |
+
|
| 74 |
+
print(f"=== MESSAGE {headers.get('Message-ID', args.message_id)} ===")
|
| 75 |
+
for k, v in headers.items():
|
| 76 |
+
print(f"{k}: {v}")
|
| 77 |
+
print("\n---")
|
| 78 |
+
print(body)
|
| 79 |
+
|
| 80 |
+
att_dir = path.parent / path.stem
|
| 81 |
+
if att_dir.is_dir():
|
| 82 |
+
print("\n=== ATTACHMENTS ===")
|
| 83 |
+
for af in sorted(att_dir.iterdir()):
|
| 84 |
+
print(f" {af.name} ({af.stat().st_size} bytes)")
|
| 85 |
+
print(f" Path: {af}")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def cmd_send(args):
|
| 89 |
+
cfg = load_config()
|
| 90 |
+
require_identity(cfg)
|
| 91 |
+
d = today_str()
|
| 92 |
+
ensure_dirs(cfg, d)
|
| 93 |
+
from_a = cfg["agent_name"]
|
| 94 |
+
to_a = args.to
|
| 95 |
+
subject = args.subject
|
| 96 |
+
|
| 97 |
+
if args.body:
|
| 98 |
+
body = args.body
|
| 99 |
+
elif args.body_file:
|
| 100 |
+
body = Path(args.body_file).read_text(encoding="utf-8")
|
| 101 |
+
elif not sys.stdin.isatty():
|
| 102 |
+
body = sys.stdin.read()
|
| 103 |
+
else:
|
| 104 |
+
print("ERROR: Provide --body, --body-file, or pipe to stdin.")
|
| 105 |
+
sys.exit(1)
|
| 106 |
+
|
| 107 |
+
attachments = args.attach or []
|
| 108 |
+
content, msg_id = format_message(from_a, to_a, subject, body, attachments=attachments)
|
| 109 |
+
outbox = agent_day(cfg, d) / OUTBOX
|
| 110 |
+
fpath = outbox / f"{msg_id}.txt"
|
| 111 |
+
|
| 112 |
+
if attachments:
|
| 113 |
+
adir = outbox / msg_id
|
| 114 |
+
adir.mkdir(exist_ok=True)
|
| 115 |
+
for ap in attachments:
|
| 116 |
+
src = Path(ap)
|
| 117 |
+
if src.exists():
|
| 118 |
+
shutil.copy2(str(src), str(adir / src.name))
|
| 119 |
+
else:
|
| 120 |
+
print(f" WARNING: {ap} not found")
|
| 121 |
+
|
| 122 |
+
tmp = fpath.with_suffix(".tmp")
|
| 123 |
+
tmp.write_text(content, encoding="utf-8")
|
| 124 |
+
tmp.rename(fpath)
|
| 125 |
+
|
| 126 |
+
build_index(cfg, d)
|
| 127 |
+
print("Message queued.")
|
| 128 |
+
print(f" ID: {msg_id}")
|
| 129 |
+
print(f" To: {to_a}")
|
| 130 |
+
print(f" Subject: {subject}")
|
| 131 |
+
if attachments:
|
| 132 |
+
print(f" Attachments: {', '.join(Path(a).name for a in attachments)}")
|
| 133 |
+
print(f" Path: {fpath}")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def cmd_reply(args):
|
| 137 |
+
cfg = load_config()
|
| 138 |
+
require_identity(cfg)
|
| 139 |
+
path = find_message(cfg, args.message_id)
|
| 140 |
+
if not path:
|
| 141 |
+
print(f"ERROR: Message '{args.message_id}' not found.")
|
| 142 |
+
sys.exit(1)
|
| 143 |
+
headers, orig_body = parse_message(path)
|
| 144 |
+
if not headers or not headers.get("From"):
|
| 145 |
+
print("ERROR: Cannot determine recipient.")
|
| 146 |
+
sys.exit(1)
|
| 147 |
+
|
| 148 |
+
to_a = headers["From"]
|
| 149 |
+
subject = headers.get("Subject", "")
|
| 150 |
+
if not subject.startswith("Re: "):
|
| 151 |
+
subject = f"Re: {subject}"
|
| 152 |
+
|
| 153 |
+
if args.body:
|
| 154 |
+
body = args.body
|
| 155 |
+
elif not sys.stdin.isatty():
|
| 156 |
+
body = sys.stdin.read()
|
| 157 |
+
else:
|
| 158 |
+
print("ERROR: Provide --body or pipe to stdin.")
|
| 159 |
+
sys.exit(1)
|
| 160 |
+
|
| 161 |
+
body += f"\n\n--- Original from {headers.get('From', '?')} ({headers.get('Date', '?')}) ---\n{orig_body}"
|
| 162 |
+
|
| 163 |
+
d = today_str()
|
| 164 |
+
ensure_dirs(cfg, d)
|
| 165 |
+
content, new_id = format_message(cfg["agent_name"], to_a, subject, body)
|
| 166 |
+
outbox = agent_day(cfg, d) / OUTBOX
|
| 167 |
+
fpath = outbox / f"{new_id}.txt"
|
| 168 |
+
fpath.write_text(content, encoding="utf-8")
|
| 169 |
+
build_index(cfg, d)
|
| 170 |
+
print("Reply queued.")
|
| 171 |
+
print(f" ID: {new_id}")
|
| 172 |
+
print(f" To: {to_a}")
|
| 173 |
+
print(f" Subject: {subject}")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def cmd_dates(args):
|
| 177 |
+
cfg = load_config()
|
| 178 |
+
dirs = date_dirs(cfg)
|
| 179 |
+
if not dirs:
|
| 180 |
+
print("No dates yet.")
|
| 181 |
+
return
|
| 182 |
+
print(f"=== Dates for {cfg['agent_name']} ===")
|
| 183 |
+
b = agent_base(cfg)
|
| 184 |
+
for d in dirs:
|
| 185 |
+
dd = b / d
|
| 186 |
+
ic = len(list((dd / INBOX).glob("*.txt"))) if (dd / INBOX).exists() else 0
|
| 187 |
+
sc = len(list((dd / SENT).glob("*.txt"))) if (dd / SENT).exists() else 0
|
| 188 |
+
oc = len(list((dd / OUTBOX).glob("*.txt"))) if (dd / OUTBOX).exists() else 0
|
| 189 |
+
nc = len(list((dd / "notes").glob("*.txt"))) if (dd / "notes").exists() else 0
|
| 190 |
+
print(f" {d} | inbox:{ic} sent:{sc} pending:{oc} notes:{nc}")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def cmd_search(args):
|
| 194 |
+
cfg = load_config()
|
| 195 |
+
q = args.query.lower()
|
| 196 |
+
b = agent_base(cfg)
|
| 197 |
+
if not b.exists():
|
| 198 |
+
print("No messages to search.")
|
| 199 |
+
return
|
| 200 |
+
results = []
|
| 201 |
+
for d in date_dirs(cfg):
|
| 202 |
+
for folder in (INBOX, SENT):
|
| 203 |
+
fp = b / d / folder
|
| 204 |
+
if not fp.exists():
|
| 205 |
+
continue
|
| 206 |
+
for f in fp.glob("*.txt"):
|
| 207 |
+
h, body = parse_message(f)
|
| 208 |
+
if not h:
|
| 209 |
+
continue
|
| 210 |
+
searchable = " ".join(h.values()).lower() + " " + (body or "").lower()
|
| 211 |
+
if q in searchable:
|
| 212 |
+
results.append((d, folder, h, f))
|
| 213 |
+
if not results:
|
| 214 |
+
print(f"No results for '{args.query}'.")
|
| 215 |
+
return
|
| 216 |
+
print(f"=== Search: '{args.query}' ({len(results)} found) ===")
|
| 217 |
+
for d, folder, h, f in results:
|
| 218 |
+
direction = f"From: {h.get('From', '?')}" if folder == INBOX else f"To: {h.get('To', '?')}"
|
| 219 |
+
print(f" [{d}] [{folder.upper()}] {direction} | Subject: {h.get('Subject', '?')} | ID: {h.get('Message-ID', f.stem)}")
|
| 220 |
+
print(f" Path: {f}")
|
src/agentazall/commands/notes.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll commands: note, notes β named notes management."""
|
| 2 |
+
|
| 3 |
+
from ..config import NOTES, load_config
|
| 4 |
+
from ..finder import find_latest_file
|
| 5 |
+
from ..helpers import (
|
| 6 |
+
agent_day,
|
| 7 |
+
ensure_dirs,
|
| 8 |
+
require_identity,
|
| 9 |
+
sanitize,
|
| 10 |
+
today_str,
|
| 11 |
+
)
|
| 12 |
+
from ..index import build_index
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def cmd_note(args):
|
| 16 |
+
cfg = load_config()
|
| 17 |
+
if args.set:
|
| 18 |
+
require_identity(cfg)
|
| 19 |
+
d = today_str()
|
| 20 |
+
ensure_dirs(cfg, d)
|
| 21 |
+
name = sanitize(args.name)
|
| 22 |
+
f = agent_day(cfg, d) / NOTES / f"{name}.txt"
|
| 23 |
+
|
| 24 |
+
if args.set:
|
| 25 |
+
f.write_text(args.set, encoding="utf-8")
|
| 26 |
+
build_index(cfg, d)
|
| 27 |
+
print(f"Note '{name}' saved: {f}")
|
| 28 |
+
elif args.append:
|
| 29 |
+
old = f.read_text(encoding="utf-8") if f.exists() else ""
|
| 30 |
+
f.write_text((old + "\n" + args.append).lstrip("\n"), encoding="utf-8")
|
| 31 |
+
build_index(cfg, d)
|
| 32 |
+
print(f"Note '{name}' appended: {f}")
|
| 33 |
+
else:
|
| 34 |
+
if f.exists():
|
| 35 |
+
print(f.read_text(encoding="utf-8"))
|
| 36 |
+
else:
|
| 37 |
+
text = find_latest_file(cfg, f"{NOTES}/{name}.txt")
|
| 38 |
+
if text:
|
| 39 |
+
print(text)
|
| 40 |
+
else:
|
| 41 |
+
print(f"Note '{name}' not found.")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def cmd_notes(args):
|
| 45 |
+
cfg = load_config()
|
| 46 |
+
d = args.date or today_str()
|
| 47 |
+
nd = agent_day(cfg, d) / NOTES
|
| 48 |
+
if not nd.exists() or not list(nd.glob("*.txt")):
|
| 49 |
+
print(f"No notes for {d}.")
|
| 50 |
+
return
|
| 51 |
+
print(f"=== Notes | {d} ===")
|
| 52 |
+
for f in sorted(nd.glob("*.txt")):
|
| 53 |
+
print(f" {f.stem} ({f.stat().st_size}B) | {f}")
|
src/agentazall/config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll configuration β constants, config resolution, load/save."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
VERSION = "1.0.0"
|
| 9 |
+
|
| 10 |
+
# ββ folder name constants ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
|
| 12 |
+
INBOX = "inbox"
|
| 13 |
+
OUTBOX = "outbox"
|
| 14 |
+
SENT = "sent"
|
| 15 |
+
WHO_AM_I = "who_am_i"
|
| 16 |
+
WHAT_AM_I_DOING = "what_am_i_doing"
|
| 17 |
+
NOTES = "notes"
|
| 18 |
+
REMEMBER = "remember"
|
| 19 |
+
SKILLS = "skills"
|
| 20 |
+
TOOLS = "tools"
|
| 21 |
+
INDEX = "index.txt"
|
| 22 |
+
REMEMBER_INDEX = "remember_index.txt"
|
| 23 |
+
SEEN_FILE = ".seen_ids"
|
| 24 |
+
ALL_SUBDIRS = (INBOX, OUTBOX, SENT, WHO_AM_I, WHAT_AM_I_DOING, NOTES, REMEMBER)
|
| 25 |
+
AGENT_LEVEL_DIRS = (SKILLS, TOOLS)
|
| 26 |
+
|
| 27 |
+
MAX_SEEN_IDS = 10000
|
| 28 |
+
|
| 29 |
+
LOG_FMT = "%(asctime)s [%(levelname)s] %(message)s"
|
| 30 |
+
|
| 31 |
+
# ββ default config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
|
| 33 |
+
DEFAULT_CONFIG = {
|
| 34 |
+
"agent_name": "agent1@localhost",
|
| 35 |
+
"agent_key": "",
|
| 36 |
+
"allow_memory_sharing": False,
|
| 37 |
+
"mailbox_dir": "./data/mailboxes",
|
| 38 |
+
"transport": "email",
|
| 39 |
+
"sync_interval": 10,
|
| 40 |
+
"log_file": "./logs/agentazall.log",
|
| 41 |
+
"email": {
|
| 42 |
+
"imap_server": "127.0.0.1",
|
| 43 |
+
"imap_port": 1143,
|
| 44 |
+
"imap_ssl": False,
|
| 45 |
+
"imap_folder": "INBOX",
|
| 46 |
+
"smtp_server": "127.0.0.1",
|
| 47 |
+
"smtp_port": 2525,
|
| 48 |
+
"smtp_ssl": False,
|
| 49 |
+
"smtp_starttls": False,
|
| 50 |
+
"pop3_server": "127.0.0.1",
|
| 51 |
+
"pop3_port": 1110,
|
| 52 |
+
"pop3_ssl": False,
|
| 53 |
+
"use_pop3": False,
|
| 54 |
+
"username": "agent1@localhost",
|
| 55 |
+
"password": "password",
|
| 56 |
+
"sync_special_folders": True,
|
| 57 |
+
},
|
| 58 |
+
"ftp": {
|
| 59 |
+
"host": "127.0.0.1",
|
| 60 |
+
"port": 2121,
|
| 61 |
+
"port_range": [2121, 2199],
|
| 62 |
+
"user": "agentoftp",
|
| 63 |
+
"password": "agentoftp_pass",
|
| 64 |
+
"root": "./data/ftp_root",
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ββ config resolution ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
|
| 71 |
+
def resolve_config_path() -> Path:
|
| 72 |
+
"""Resolve the config file path.
|
| 73 |
+
|
| 74 |
+
Priority:
|
| 75 |
+
1. AGENTAZALL_CONFIG env var β explicit path
|
| 76 |
+
2. AGENTAZALL_ROOT env var β $ROOT/config.json
|
| 77 |
+
3. ./config.json β cwd fallback
|
| 78 |
+
"""
|
| 79 |
+
env_config = os.environ.get("AGENTAZALL_CONFIG")
|
| 80 |
+
if env_config:
|
| 81 |
+
return Path(env_config)
|
| 82 |
+
env_root = os.environ.get("AGENTAZALL_ROOT")
|
| 83 |
+
if env_root:
|
| 84 |
+
return Path(env_root) / "config.json"
|
| 85 |
+
return Path.cwd() / "config.json"
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _deep_merge(base: dict, override: dict) -> dict:
|
| 89 |
+
"""Recursively merge override into base, returning a new dict."""
|
| 90 |
+
out = dict(base)
|
| 91 |
+
for k, v in override.items():
|
| 92 |
+
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
|
| 93 |
+
out[k] = _deep_merge(out[k], v)
|
| 94 |
+
else:
|
| 95 |
+
out[k] = v
|
| 96 |
+
return out
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _resolve_relative_paths(cfg: dict, config_dir: Path):
|
| 100 |
+
"""Resolve relative paths in config relative to the config file's directory."""
|
| 101 |
+
for key in ("mailbox_dir", "log_file"):
|
| 102 |
+
val = cfg.get(key, "")
|
| 103 |
+
if val and not os.path.isabs(val):
|
| 104 |
+
cfg[key] = str((config_dir / val).resolve())
|
| 105 |
+
if "ftp" in cfg:
|
| 106 |
+
root = cfg["ftp"].get("root", "")
|
| 107 |
+
if root and not os.path.isabs(root):
|
| 108 |
+
cfg["ftp"]["root"] = str((config_dir / root).resolve())
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def load_config(config_path: Path = None) -> dict:
|
| 112 |
+
"""Load and merge config, resolving relative paths."""
|
| 113 |
+
if config_path is None:
|
| 114 |
+
config_path = resolve_config_path()
|
| 115 |
+
config_path = Path(config_path)
|
| 116 |
+
if not config_path.exists():
|
| 117 |
+
print(f"ERROR: No config at {config_path}")
|
| 118 |
+
print("Run: agentazall setup --agent <name>")
|
| 119 |
+
sys.exit(1)
|
| 120 |
+
with open(config_path, encoding="utf-8") as f:
|
| 121 |
+
user = json.load(f)
|
| 122 |
+
cfg = _deep_merge(DEFAULT_CONFIG, user)
|
| 123 |
+
_resolve_relative_paths(cfg, config_path.parent.resolve())
|
| 124 |
+
env = os.environ.get("AGENTAZALL_AGENT")
|
| 125 |
+
if env:
|
| 126 |
+
cfg["agent_name"] = env
|
| 127 |
+
# stash config path for save_config
|
| 128 |
+
cfg["_config_path"] = str(config_path.resolve())
|
| 129 |
+
return cfg
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def save_config(cfg: dict, config_path: Path = None):
|
| 133 |
+
"""Write config to disk (strips internal keys)."""
|
| 134 |
+
if config_path is None:
|
| 135 |
+
config_path = Path(cfg.get("_config_path", str(resolve_config_path())))
|
| 136 |
+
config_path = Path(config_path)
|
| 137 |
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
| 138 |
+
out = {k: v for k, v in cfg.items() if not k.startswith("_")}
|
| 139 |
+
with open(config_path, "w", encoding="utf-8") as f:
|
| 140 |
+
json.dump(out, f, indent=2)
|
src/agentazall/finder.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll message finder β locate messages and manage seen IDs."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from .config import INBOX, MAX_SEEN_IDS, OUTBOX, SEEN_FILE, SENT
|
| 8 |
+
from .helpers import agent_base, date_dirs
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def find_message(cfg, msg_id, d=None) -> Optional[Path]:
|
| 12 |
+
"""Find a message file by ID, searching across dates and folders."""
|
| 13 |
+
b = agent_base(cfg)
|
| 14 |
+
if not b.exists():
|
| 15 |
+
return None
|
| 16 |
+
dates = [d] if d else sorted(
|
| 17 |
+
(x.name for x in b.iterdir()
|
| 18 |
+
if x.is_dir() and re.match(r"\d{4}-\d{2}-\d{2}$", x.name)),
|
| 19 |
+
reverse=True,
|
| 20 |
+
)
|
| 21 |
+
for dd in dates:
|
| 22 |
+
for folder in (INBOX, SENT, OUTBOX):
|
| 23 |
+
exact = b / dd / folder / f"{msg_id}.txt"
|
| 24 |
+
if exact.exists():
|
| 25 |
+
return exact
|
| 26 |
+
fp = b / dd / folder
|
| 27 |
+
if fp.exists():
|
| 28 |
+
for f in fp.glob("*.txt"):
|
| 29 |
+
if msg_id in f.stem:
|
| 30 |
+
return f
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def find_latest_file(cfg, rel_path) -> Optional[str]:
|
| 35 |
+
"""Find the latest version of a file across all date directories."""
|
| 36 |
+
for d in reversed(date_dirs(cfg)):
|
| 37 |
+
fp = agent_base(cfg) / d / rel_path
|
| 38 |
+
if fp.exists():
|
| 39 |
+
return fp.read_text(encoding="utf-8")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ββ seen IDs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
|
| 45 |
+
def load_seen(cfg) -> set:
|
| 46 |
+
p = agent_base(cfg) / SEEN_FILE
|
| 47 |
+
if p.exists():
|
| 48 |
+
return set(p.read_text(encoding="utf-8").strip().splitlines())
|
| 49 |
+
return set()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def save_seen(cfg, seen: set):
|
| 53 |
+
if len(seen) > MAX_SEEN_IDS:
|
| 54 |
+
seen_list = sorted(seen)
|
| 55 |
+
seen.clear()
|
| 56 |
+
seen.update(seen_list[-MAX_SEEN_IDS:])
|
| 57 |
+
p = agent_base(cfg) / SEEN_FILE
|
| 58 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
p.write_text("\n".join(sorted(seen)), encoding="utf-8")
|
src/agentazall/helpers.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll helpers β date utils, path helpers, identity validation."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
+
from datetime import date, datetime
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import List
|
| 11 |
+
|
| 12 |
+
from .config import (
|
| 13 |
+
AGENT_LEVEL_DIRS,
|
| 14 |
+
ALL_SUBDIRS,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# ββ date/time ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
|
| 19 |
+
def today_str() -> str:
|
| 20 |
+
return date.today().isoformat()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def now_str() -> str:
|
| 24 |
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ββ path helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
|
| 29 |
+
def agent_base(cfg) -> Path:
|
| 30 |
+
return Path(cfg["mailbox_dir"]) / cfg["agent_name"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def agent_day(cfg, d=None) -> Path:
|
| 34 |
+
return agent_base(cfg) / (d or today_str())
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def shared_dir(cfg) -> Path:
|
| 38 |
+
"""Return the shared tools/skills root: data/shared/"""
|
| 39 |
+
return Path(cfg["mailbox_dir"]).parent / "shared"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def ensure_dirs(cfg, d=None) -> Path:
|
| 43 |
+
root = agent_day(cfg, d)
|
| 44 |
+
for sub in ALL_SUBDIRS:
|
| 45 |
+
(root / sub).mkdir(parents=True, exist_ok=True)
|
| 46 |
+
base = agent_base(cfg)
|
| 47 |
+
for sub in AGENT_LEVEL_DIRS:
|
| 48 |
+
(base / sub).mkdir(parents=True, exist_ok=True)
|
| 49 |
+
return root
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def date_dirs(cfg) -> List[str]:
|
| 53 |
+
b = agent_base(cfg)
|
| 54 |
+
if not b.exists():
|
| 55 |
+
return []
|
| 56 |
+
return sorted(
|
| 57 |
+
d.name for d in b.iterdir()
|
| 58 |
+
if d.is_dir() and re.match(r"\d{4}-\d{2}-\d{2}$", d.name)
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ββ id / sanitization βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 63 |
+
|
| 64 |
+
def generate_id(from_a, to_a, subject) -> str:
|
| 65 |
+
raw = f"{from_a}|{to_a}|{subject}|{datetime.now().isoformat()}|{os.urandom(8).hex()}"
|
| 66 |
+
return hashlib.sha256(raw.encode()).hexdigest()[:12]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def sanitize(name: str) -> str:
|
| 70 |
+
return re.sub(r'[^\w\-.]', '_', name)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def safe_move(src: str, dst: str):
|
| 74 |
+
"""Move file safely on Windows (copy+remove fallback)."""
|
| 75 |
+
try:
|
| 76 |
+
shutil.move(src, dst)
|
| 77 |
+
except (PermissionError, OSError):
|
| 78 |
+
shutil.copy2(src, dst)
|
| 79 |
+
os.remove(src)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# ββ identity validation βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 83 |
+
|
| 84 |
+
def validate_agent_key(cfg: dict) -> bool:
|
| 85 |
+
"""Verify that the config's agent_key matches the key stored in the mailbox."""
|
| 86 |
+
key_file = agent_base(cfg) / ".agent_key"
|
| 87 |
+
if not key_file.exists():
|
| 88 |
+
return True # legacy agent without key
|
| 89 |
+
try:
|
| 90 |
+
stored = json.loads(key_file.read_text(encoding="utf-8"))
|
| 91 |
+
config_key = cfg.get("agent_key", "")
|
| 92 |
+
if not config_key:
|
| 93 |
+
return True # legacy config without key
|
| 94 |
+
return stored.get("key") == config_key
|
| 95 |
+
except Exception:
|
| 96 |
+
return True # don't block on corrupted key files
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def require_identity(cfg: dict):
|
| 100 |
+
"""Validate agent key before any write operation. Exit if invalid."""
|
| 101 |
+
import sys
|
| 102 |
+
if not validate_agent_key(cfg):
|
| 103 |
+
agent = cfg.get("agent_name", "unknown")
|
| 104 |
+
print(f"ERROR: Identity verification failed for '{agent}'.")
|
| 105 |
+
print("Your config key does not match the agent's registered key.")
|
| 106 |
+
print("You cannot write to another agent's space.")
|
| 107 |
+
sys.exit(1)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def can_read_agent_memories(cfg: dict, target_agent: str) -> bool:
|
| 111 |
+
"""Check if current agent is allowed to read target agent's memories."""
|
| 112 |
+
if cfg["agent_name"] == target_agent:
|
| 113 |
+
return True
|
| 114 |
+
target_base = Path(cfg["mailbox_dir"]) / target_agent
|
| 115 |
+
key_file = target_base / ".agent_key"
|
| 116 |
+
if key_file.exists():
|
| 117 |
+
try:
|
| 118 |
+
stored = json.loads(key_file.read_text(encoding="utf-8"))
|
| 119 |
+
return stored.get("allow_memory_sharing", False)
|
| 120 |
+
except Exception:
|
| 121 |
+
pass
|
| 122 |
+
return False
|
src/agentazall/index.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll index builder β daily index and cross-day memory index."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from .config import (
|
| 8 |
+
INBOX,
|
| 9 |
+
INDEX,
|
| 10 |
+
NOTES,
|
| 11 |
+
OUTBOX,
|
| 12 |
+
REMEMBER,
|
| 13 |
+
REMEMBER_INDEX,
|
| 14 |
+
SENT,
|
| 15 |
+
SKILLS,
|
| 16 |
+
TOOLS,
|
| 17 |
+
WHAT_AM_I_DOING,
|
| 18 |
+
WHO_AM_I,
|
| 19 |
+
)
|
| 20 |
+
from .helpers import (
|
| 21 |
+
agent_base,
|
| 22 |
+
agent_day,
|
| 23 |
+
date_dirs,
|
| 24 |
+
now_str,
|
| 25 |
+
today_str,
|
| 26 |
+
)
|
| 27 |
+
from .messages import parse_headers_only
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def build_index(cfg, d=None) -> Optional[Path]:
|
| 31 |
+
"""Build or rebuild the daily index file."""
|
| 32 |
+
d = d or today_str()
|
| 33 |
+
root = agent_day(cfg, d)
|
| 34 |
+
if not root.exists():
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
lines = [
|
| 38 |
+
f"# AgentAZAll Index: {cfg['agent_name']}",
|
| 39 |
+
f"# Date: {d}",
|
| 40 |
+
f"# Updated: {now_str()}",
|
| 41 |
+
"",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
# inbox
|
| 45 |
+
inbox_dir = root / INBOX
|
| 46 |
+
ie = []
|
| 47 |
+
if inbox_dir.exists():
|
| 48 |
+
for f in sorted(inbox_dir.glob("*.txt")):
|
| 49 |
+
h = parse_headers_only(f)
|
| 50 |
+
if not h:
|
| 51 |
+
continue
|
| 52 |
+
st = h.get("Status", "?").upper()
|
| 53 |
+
ts = h.get("Date", "").split()[-1] if " " in h.get("Date", "") else "??:??"
|
| 54 |
+
att = " [ATT]" if "Attachments" in h else ""
|
| 55 |
+
ie.append(
|
| 56 |
+
f" [{st}]{att} {ts} | From: {h.get('From', '?')} "
|
| 57 |
+
f"| Subject: {h.get('Subject', '?')} | {INBOX}/{f.name}"
|
| 58 |
+
)
|
| 59 |
+
lines.append(f"INBOX ({len(ie)}):")
|
| 60 |
+
lines.extend(ie or [" (empty)"])
|
| 61 |
+
lines.append("")
|
| 62 |
+
|
| 63 |
+
# sent
|
| 64 |
+
sent_dir = root / SENT
|
| 65 |
+
se = []
|
| 66 |
+
if sent_dir.exists():
|
| 67 |
+
for f in sorted(sent_dir.glob("*.txt")):
|
| 68 |
+
h = parse_headers_only(f)
|
| 69 |
+
if not h:
|
| 70 |
+
continue
|
| 71 |
+
ts = h.get("Date", "").split()[-1] if " " in h.get("Date", "") else "??:??"
|
| 72 |
+
se.append(
|
| 73 |
+
f" {ts} | To: {h.get('To', '?')} "
|
| 74 |
+
f"| Subject: {h.get('Subject', '?')} | {SENT}/{f.name}"
|
| 75 |
+
)
|
| 76 |
+
lines.append(f"SENT ({len(se)}):")
|
| 77 |
+
lines.extend(se or [" (empty)"])
|
| 78 |
+
lines.append("")
|
| 79 |
+
|
| 80 |
+
# outbox (pending)
|
| 81 |
+
outbox_dir = root / OUTBOX
|
| 82 |
+
oe = []
|
| 83 |
+
if outbox_dir.exists():
|
| 84 |
+
for f in sorted(outbox_dir.glob("*.txt")):
|
| 85 |
+
h = parse_headers_only(f)
|
| 86 |
+
if not h:
|
| 87 |
+
continue
|
| 88 |
+
oe.append(
|
| 89 |
+
f" [PENDING] To: {h.get('To', '?')} "
|
| 90 |
+
f"| Subject: {h.get('Subject', '?')} | {OUTBOX}/{f.name}"
|
| 91 |
+
)
|
| 92 |
+
if oe:
|
| 93 |
+
lines.append(f"OUTBOX ({len(oe)}):")
|
| 94 |
+
lines.extend(oe)
|
| 95 |
+
lines.append("")
|
| 96 |
+
|
| 97 |
+
# notes
|
| 98 |
+
notes_dir = root / NOTES
|
| 99 |
+
ne = []
|
| 100 |
+
if notes_dir.exists():
|
| 101 |
+
for f in sorted(notes_dir.glob("*.txt")):
|
| 102 |
+
ne.append(f" {f.stem} ({f.stat().st_size}B) | {NOTES}/{f.name}")
|
| 103 |
+
if ne:
|
| 104 |
+
lines.append(f"NOTES ({len(ne)}):")
|
| 105 |
+
lines.extend(ne)
|
| 106 |
+
lines.append("")
|
| 107 |
+
|
| 108 |
+
# remember
|
| 109 |
+
rem_dir = root / REMEMBER
|
| 110 |
+
re_entries = []
|
| 111 |
+
if rem_dir.exists():
|
| 112 |
+
for f in sorted(rem_dir.glob("*.txt")):
|
| 113 |
+
text = f.read_text(encoding="utf-8", errors="replace").strip()
|
| 114 |
+
first = ""
|
| 115 |
+
for ln in text.split("\n"):
|
| 116 |
+
ln = ln.strip()
|
| 117 |
+
if ln:
|
| 118 |
+
first = ln[:100]
|
| 119 |
+
break
|
| 120 |
+
re_entries.append(f" {f.stem}: {first} | {REMEMBER}/{f.name}")
|
| 121 |
+
if re_entries:
|
| 122 |
+
lines.append(f"REMEMBER ({len(re_entries)}):")
|
| 123 |
+
lines.extend(re_entries)
|
| 124 |
+
lines.append("")
|
| 125 |
+
|
| 126 |
+
# identity / tasks
|
| 127 |
+
wf = root / WHO_AM_I / "identity.txt"
|
| 128 |
+
df = root / WHAT_AM_I_DOING / "tasks.txt"
|
| 129 |
+
if wf.exists():
|
| 130 |
+
lines.append(f"IDENTITY: {WHO_AM_I}/identity.txt")
|
| 131 |
+
if df.exists():
|
| 132 |
+
lines.append(f"TASKS: {WHAT_AM_I_DOING}/tasks.txt")
|
| 133 |
+
|
| 134 |
+
# skills & tools (agent-level, not per-day)
|
| 135 |
+
base = agent_base(cfg)
|
| 136 |
+
for kind in (SKILLS, TOOLS):
|
| 137 |
+
kdir = base / kind
|
| 138 |
+
if kdir.exists():
|
| 139 |
+
entries = sorted(kdir.glob("*.py"))
|
| 140 |
+
if entries:
|
| 141 |
+
lines.append("")
|
| 142 |
+
lines.append(f"{kind.upper()} ({len(entries)}):")
|
| 143 |
+
for f in entries:
|
| 144 |
+
meta = {}
|
| 145 |
+
mp = f.with_suffix(".meta.json")
|
| 146 |
+
if mp.exists():
|
| 147 |
+
try:
|
| 148 |
+
meta = json.loads(mp.read_text(encoding="utf-8"))
|
| 149 |
+
except Exception:
|
| 150 |
+
pass
|
| 151 |
+
desc = meta.get("description", "")
|
| 152 |
+
tag = f"- {desc[:60]}" if desc else ""
|
| 153 |
+
lines.append(f" {f.stem} {tag}")
|
| 154 |
+
|
| 155 |
+
content = "\n".join(lines)
|
| 156 |
+
idx = root / INDEX
|
| 157 |
+
idx.write_text(content, encoding="utf-8")
|
| 158 |
+
return idx
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# ββ remember index (cross-day sparse bullet-point index) βββββββββββββββββββββ
|
| 162 |
+
|
| 163 |
+
def _remember_needs_rebuild(cfg) -> bool:
|
| 164 |
+
"""Check if any remember files are newer than the index."""
|
| 165 |
+
b = agent_base(cfg)
|
| 166 |
+
idx = b / REMEMBER_INDEX
|
| 167 |
+
if not idx.exists():
|
| 168 |
+
return True
|
| 169 |
+
idx_mtime = idx.stat().st_mtime
|
| 170 |
+
for d in date_dirs(cfg):
|
| 171 |
+
rem_dir = b / d / REMEMBER
|
| 172 |
+
if not rem_dir.exists():
|
| 173 |
+
continue
|
| 174 |
+
if rem_dir.stat().st_mtime > idx_mtime:
|
| 175 |
+
return True
|
| 176 |
+
for f in rem_dir.glob("*.txt"):
|
| 177 |
+
if f.stat().st_mtime > idx_mtime:
|
| 178 |
+
return True
|
| 179 |
+
return False
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def build_remember_index(cfg) -> Optional[Path]:
|
| 183 |
+
"""Build a consolidated sparse bullet-point memory index across all days."""
|
| 184 |
+
b = agent_base(cfg)
|
| 185 |
+
if not b.exists():
|
| 186 |
+
return None
|
| 187 |
+
|
| 188 |
+
if not _remember_needs_rebuild(cfg):
|
| 189 |
+
return b / REMEMBER_INDEX
|
| 190 |
+
|
| 191 |
+
entries = []
|
| 192 |
+
for d in sorted(date_dirs(cfg), reverse=True):
|
| 193 |
+
rem_dir = b / d / REMEMBER
|
| 194 |
+
if not rem_dir.exists():
|
| 195 |
+
continue
|
| 196 |
+
for f in sorted(rem_dir.glob("*.txt"), reverse=True):
|
| 197 |
+
text = f.read_text(encoding="utf-8", errors="replace").strip()
|
| 198 |
+
summary = ""
|
| 199 |
+
for ln in text.split("\n"):
|
| 200 |
+
ln = ln.strip()
|
| 201 |
+
if ln:
|
| 202 |
+
summary = ln[:120]
|
| 203 |
+
break
|
| 204 |
+
title = f.stem
|
| 205 |
+
entries.append((d, title, summary, f"{d}/{REMEMBER}/{f.name}"))
|
| 206 |
+
|
| 207 |
+
lines = [
|
| 208 |
+
f"# Agent Memory Index: {cfg['agent_name']}",
|
| 209 |
+
f"# Updated: {now_str()}",
|
| 210 |
+
f"# Total memories: {len(entries)}",
|
| 211 |
+
"",
|
| 212 |
+
]
|
| 213 |
+
for d, title, summary, rel in entries:
|
| 214 |
+
lines.append(f"- [{d}] {title}: {summary} | {rel}")
|
| 215 |
+
|
| 216 |
+
if not entries:
|
| 217 |
+
lines.append("(no memories stored yet)")
|
| 218 |
+
|
| 219 |
+
idx = b / REMEMBER_INDEX
|
| 220 |
+
idx.parent.mkdir(parents=True, exist_ok=True)
|
| 221 |
+
idx.write_text("\n".join(lines), encoding="utf-8")
|
| 222 |
+
return idx
|
src/agentazall/messages.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AgentAZAll message format β compose & parse plain-text messages."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional, Tuple
|
| 5 |
+
|
| 6 |
+
from .helpers import generate_id, now_str
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def format_message(from_a, to_a, subject, body, msg_id=None, attachments=None) -> Tuple[str, str]:
|
| 10 |
+
"""Build a plain-text message string. Returns (content, msg_id)."""
|
| 11 |
+
if not msg_id:
|
| 12 |
+
msg_id = generate_id(from_a, to_a, subject)
|
| 13 |
+
lines = [
|
| 14 |
+
f"From: {from_a}",
|
| 15 |
+
f"To: {to_a}",
|
| 16 |
+
f"Subject: {subject}",
|
| 17 |
+
f"Date: {now_str()}",
|
| 18 |
+
f"Message-ID: {msg_id}",
|
| 19 |
+
"Status: new",
|
| 20 |
+
]
|
| 21 |
+
if attachments:
|
| 22 |
+
lines.append(f"Attachments: {', '.join(Path(a).name for a in attachments)}")
|
| 23 |
+
lines += ["", "---", body]
|
| 24 |
+
return "\n".join(lines), msg_id
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def parse_message(path) -> Tuple[Optional[dict], Optional[str]]:
|
| 28 |
+
"""Parse a message file into (headers_dict, body_text)."""
|
| 29 |
+
p = Path(path)
|
| 30 |
+
if not p.exists():
|
| 31 |
+
return None, None
|
| 32 |
+
text = p.read_text(encoding="utf-8", errors="replace")
|
| 33 |
+
headers: dict = {}
|
| 34 |
+
body_lines: list = []
|
| 35 |
+
in_body = False
|
| 36 |
+
for line in text.split("\n"):
|
| 37 |
+
if not in_body:
|
| 38 |
+
if line.strip() == "---":
|
| 39 |
+
in_body = True
|
| 40 |
+
continue
|
| 41 |
+
if ":" in line:
|
| 42 |
+
k, _, v = line.partition(":")
|
| 43 |
+
headers[k.strip()] = v.strip()
|
| 44 |
+
else:
|
| 45 |
+
body_lines.append(line)
|
| 46 |
+
return headers, "\n".join(body_lines)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def parse_headers_only(path) -> dict:
|
| 50 |
+
"""Parse only message headers (faster β stops at '---')."""
|
| 51 |
+
headers = {}
|
| 52 |
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
| 53 |
+
for line in f:
|
| 54 |
+
line = line.rstrip("\n")
|
| 55 |
+
if line.strip() == "---":
|
| 56 |
+
break
|
| 57 |
+
if ":" in line:
|
| 58 |
+
k, _, v = line.partition(":")
|
| 59 |
+
headers[k.strip()] = v.strip()
|
| 60 |
+
return headers
|