cronos3k commited on
Commit
c75bbd7
Β·
verified Β·
1 Parent(s): 05176f5

Upload folder using huggingface_hub

Browse files
Files changed (31) hide show
  1. README.md +53 -7
  2. app.py +443 -0
  3. demo_data/.seeded +1 -0
  4. demo_data/config.json +18 -0
  5. demo_data/mailboxes/demo-agent@localhost/2026-03-08/inbox/33d7dea804da.txt +15 -0
  6. demo_data/mailboxes/demo-agent@localhost/2026-03-08/notes/handoff.txt +1 -0
  7. demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/architecture.txt +1 -0
  8. demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/capabilities.txt +1 -0
  9. demo_data/mailboxes/demo-agent@localhost/2026-03-08/remember/observation-patterns.txt +1 -0
  10. demo_data/mailboxes/demo-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
  11. demo_data/mailboxes/demo-agent@localhost/2026-03-08/who_am_i/identity.txt +1 -0
  12. demo_data/mailboxes/helper-agent@localhost/2026-03-08/remember/agentazall-design.txt +1 -0
  13. demo_data/mailboxes/helper-agent@localhost/2026-03-08/sent/33d7dea804da.txt +15 -0
  14. demo_data/mailboxes/helper-agent@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
  15. demo_data/mailboxes/helper-agent@localhost/2026-03-08/who_am_i/identity.txt +1 -0
  16. demo_data/mailboxes/visitor@localhost/2026-03-08/what_am_i_doing/tasks.txt +1 -0
  17. demo_data/mailboxes/visitor@localhost/2026-03-08/who_am_i/identity.txt +1 -0
  18. llm_bridge.py +414 -0
  19. requirements.txt +5 -0
  20. seed_data.py +218 -0
  21. src/agentazall/__init__.py +3 -0
  22. src/agentazall/commands/__init__.py +1 -0
  23. src/agentazall/commands/identity.py +49 -0
  24. src/agentazall/commands/memory.py +106 -0
  25. src/agentazall/commands/messaging.py +220 -0
  26. src/agentazall/commands/notes.py +53 -0
  27. src/agentazall/config.py +140 -0
  28. src/agentazall/finder.py +59 -0
  29. src/agentazall/helpers.py +122 -0
  30. src/agentazall/index.py +222 -0
  31. src/agentazall/messages.py +60 -0
README.md CHANGED
@@ -1,13 +1,59 @@
1
  ---
2
- title: AgentAZAll
3
- emoji: ⚑
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.9.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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