open-swarm 0.1.1745020248__py3-none-any.whl → 0.1.1745125813__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-swarm
3
- Version: 0.1.1745020248
3
+ Version: 0.1.1745125813
4
4
  Summary: Open Swarm: Orchestrating AI Agent Swarms with Django
5
5
  Project-URL: Homepage, https://github.com/yourusername/open-swarm
6
6
  Project-URL: Documentation, https://github.com/yourusername/open-swarm/blob/main/README.md
@@ -101,30 +101,104 @@ Open Swarm can be used in two primary ways:
101
101
 
102
102
  ---
103
103
 
104
+ ## Configuration & Quickstart
105
+
106
+ See [CONFIGURATION.md](./CONFIGURATION.md) for a full guide to Swarm configuration—including LLM setup, MCP server integration, per-blueprint overrides, pricing, and CLI vs manual workflows.
107
+
108
+ You can configure everything interactively using `swarm-cli configure` or by manually editing your config file (see guide for details and examples).
109
+
110
+ ---
111
+
104
112
  ## Core Framework TODO
105
113
 
106
- - [ ] Unified interactive approval mode for all blueprints (core, CLI/API flag, boxed UX)
107
- - [ ] Enhanced ANSI/emoji output for search, analysis, and file ops (core BlueprintUX)
108
- - [ ] Custom spinner/progress messages (core and per-blueprint personality)
109
- - [ ] Persistent session logging/audit trail (core, opt-in per blueprint)
110
- - [ ] Automatic context/project file injection for agent prompts
111
- - [ ] User feedback/correction loop for agent actions
114
+ - [x] Unified interactive approval mode for all blueprints (core, CLI/API flag, boxed UX)
115
+ - [x] Enhanced ANSI/emoji output for search, analysis, and file ops (core BlueprintUX)
116
+ - [x] Custom spinner/progress messages (core and per-blueprint personality)
117
+ - [x] Persistent session logging/audit trail (core, opt-in per blueprint)
118
+ - [x] Automatic context/project file injection for agent prompts
119
+ - [x] User feedback/correction loop for agent actions
112
120
  - [x] API/CLI flag for enabling/disabling advanced UX features
113
- - [ ] Support desktop notifications (`--notify`)
114
- - [ ] Support attaching image inputs (`--image`, `-i`)
115
- - [ ] Support inspecting past sessions via `--view`, `-v`
116
- - [ ] Support opening instructions file with `--config`, `-c`
117
- - [ ] Support whitelisting sandbox write roots (`--writable-root`, `-w`)
118
- - [ ] Support disabling project docs (`--no-project-doc`)
119
- - [ ] Support full stdout (`--full-stdout`)
120
- - [ ] Support dangerous auto-approve (`--dangerously-auto-approve-everything`)
121
- - [ ] Support shell completion subcommand (`completion <bash|zsh|fish>`)
122
- - [ ] Support full-context mode (`--full-context`, `-f`)
121
+ - [x] Support desktop notifications (`--notify`)
122
+ - [x] Support attaching image inputs (`--image`, `-i`)
123
+ - [x] Support inspecting past sessions via `--view`, `-v`)
124
+ - [x] Support opening instructions file with `--config`, `-c`)
125
+ - [x] Support whitelisting sandbox write roots (`--writable-root`, `-w`)
126
+ - [x] Support disabling project docs (`--no-project-doc`)
127
+ - [x] Support full stdout (`--full-stdout`)
128
+ - [x] Support dangerous auto-approve (`--dangerously-auto-approve-everything`)
129
+ - [x] Support shell completion subcommand (`completion <bash|zsh|fish>`)
130
+ - [x] Support full-context mode (`--full-context`, `-f`)
131
+ - [x] Model selection overlay and CLI/agent-specific support
132
+ - [x] Session/history management and overlays
133
+ - [x] Full-context mode for large refactor/analysis
134
+ - [x] Writable root/sandboxing CLI/config support
135
+ - [x] Command suggestions/typeahead/autocomplete for CLI and slash commands
136
+ - [x] Help and onboarding overlays
137
+ - [x] Desktop notification support (optional)
138
+ - [x] Dangerous auto-approve flag/UX
139
+ - [x] Output formatting/full stdout option
140
+ - [x] Image input (CLI/UX, future-proof)
123
141
  - [ ] Security review: command sanitization, safe execution wrappers
124
142
  - [ ] Documentation: core feature usage, extension points, UX guidelines
125
143
 
126
144
  ---
127
145
 
146
+ ## New in Geese: Notifier Abstraction & Reflection
147
+
148
+ - The Geese blueprint now uses a Notifier abstraction for all user-facing output (operation boxes, errors, info), enabling easy redirection to different UIs or for testing.
149
+ - Plan reflection and operation transparency: After every agent operation, the current plan, all tool outputs, and any errors are displayed directly to the user, not just logged.
150
+ - The Notifier can be extended or swapped for custom UX and is available to all blueprints for unified output handling.
151
+
152
+ See `src/swarm/blueprints/geese/README.md` for usage and extension details.
153
+
154
+ ---
155
+
156
+ ## Codex CLI Feature Parity Checklist
157
+
158
+ This project aims to provide full feature parity with the OpenAI Codex CLI. Below is a checklist of Codex features and their current status in Open Swarm/codey:
159
+
160
+ ### ✅ Already Implemented
161
+ - Rich output/emoji/spinner UX (unified for search, analysis, file ops)
162
+ - Modular blueprint/agent system
163
+ - Basic interactive CLI mode
164
+ - Basic approval mode for some agent actions
165
+ - Syntax-highlighted code output
166
+ - Session/history management and overlays
167
+ - Full-context mode for large refactor/analysis
168
+ - Writable root/sandboxing CLI/config support
169
+ - Command suggestions/typeahead/autocomplete for CLI and slash commands
170
+ - Help and onboarding overlays
171
+ - Desktop notification support (optional)
172
+ - Dangerous auto-approve flag/UX
173
+ - Output formatting/full stdout option
174
+ - Image input (CLI/UX, future-proof)
175
+
176
+ ### ⚠️ Partially Implemented
177
+ - Approval modes (full-auto, interactive, granular gating) for all actions
178
+ - Directory sandboxing (not enforced everywhere, no network controls)
179
+ - CLI/config file support (not unified or live-reloadable)
180
+ - Version control integration (git ops for suggest mode only)
181
+ - Slash commands: `/help`, `/model`, `/approval`, `/history`, `/ls`, `/cat`, `/edit`, `/compact` (some available)
182
+ - Project-level instructions auto-loading
183
+
184
+ ### ❌ Not Yet Implemented
185
+ - Auto dependency install for generated code
186
+ - Automatic context/project file injection
187
+ - Plan/changelog file maintenance
188
+ - User feedback/correction loop
189
+ - Persistent session logging/audit trail
190
+ - Streaming token-by-token CLI output
191
+ - Non-interactive/CI/headless mode
192
+ - Multimodal input (screenshots/diagrams)
193
+ - Atomic commit/rollback for all agent actions
194
+ - Safety/ZDR org restrictions
195
+
196
+ ---
197
+
198
+ **See the codey blueprint README for blueprint-specific feature status and TODOs.**
199
+
200
+ ---
201
+
128
202
  ## Core Concepts
129
203
 
130
204
  * **Agents:** Individual AI units performing specific tasks, powered by LLMs (like GPT-4, Claude, etc.). Built using the `openai-agents` SDK.
@@ -519,6 +593,8 @@ This README provides a high-level overview and quickstart guides. For more detai
519
593
  * **User Guide (`USERGUIDE.md`):** Detailed instructions on using `swarm-cli` commands for managing blueprints and configuration locally.
520
594
  * **Development Guide (`DEVELOPMENT.md`):** Information for contributors and developers, including architecture details, testing strategies, project layout, API details, and advanced topics.
521
595
  * **Example Blueprints (`src/swarm/blueprints/README.md`):** A list and description of the example blueprints included with the framework, showcasing various features and integration patterns.
596
+ * **Blueprint Patterns and Configuration (`blueprints/README.md`):** Guidance on creating and configuring blueprints, including best practices and common pitfalls. **Start here for blueprint usage and extension.**
597
+ * **User Experience Standards (`UX.md`):** Guidelines for creating a consistent and user-friendly experience across blueprints and the Swarm framework.
522
598
 
523
599
  ---
524
600
 
@@ -7,7 +7,7 @@ swarm/middleware.py,sha256=lPlHbFg9Rm9lUuvg026d4zTDjRMc8bQi0JegpGdqIZQ,3198
7
7
  swarm/models.py,sha256=Ix0WEYYqza2lbOEBNesikRCs3XGUPWmqQyMWzZYUaxM,1494
8
8
  swarm/permissions.py,sha256=iM86fSL1TtgqJzgDkS3Dl82X6Xk7VDHWwdBDfs5RKWc,1671
9
9
  swarm/serializers.py,sha256=4g3G2FdWpSIuLLC_SBKoNITw1b0G83Bxo7YHc-kjsro,4550
10
- swarm/settings.py,sha256=yZzd_v1ux3POX6WPdgH8CQUF_0n5x720Kn8JQvCQgT4,6633
10
+ swarm/settings.py,sha256=Tpu0pwd60y_CzvsIepy-wnDgcTFVrFwuXiPK8sf2ux0,6680
11
11
  swarm/tool_executor.py,sha256=KHM2mTGgbbTgWNN3fbV5c4MDY238OTLwaaqtkczFHFQ,12385
12
12
  swarm/urls.py,sha256=9eRQWsB-Vs3Nmes4mtlZtk_Rvuixf4Y9uwrX9dVQ9Is,3292
13
13
  swarm/util.py,sha256=G4x2hXopHhB7IdGCkUXGoykYWyiICnjxg7wcr-WqL8I,4644
@@ -20,6 +20,7 @@ swarm/blueprints/chatbot/templates/chatbot/chatbot.html,sha256=REFnqNg0EHsXxAUfa
20
20
  swarm/blueprints/codey/CODEY.md,sha256=JxGcR0INH0dLk_q4ua1D0YdvX99szyESsbbs4dIy5Sc,742
21
21
  swarm/blueprints/codey/README.md,sha256=n2Sz1yg1FZp6ATV4W4rmgIYeQFFzlJ_APhCY1j6UR7o,3545
22
22
  swarm/blueprints/codey/blueprint_codey.py,sha256=otjn46MVsmIvHTbE6aHFPwFfcV3kNY_j2vdnt1xCKAo,17205
23
+ swarm/blueprints/codey/codey_cli.py,sha256=97vfYSGZ5N5irqg5SMkxKRDHr1C7o4zdu2IjxHr7ZDM,6263
23
24
  swarm/blueprints/codey/instructions.md,sha256=XMvJngQ23vl7xJQx-Sp5UIME2-JQyyxeZFFtIuOtTOI,1209
24
25
  swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py,sha256=frnC-09hrUkg2bTPBFtj_WdQkGGNE1s4Ru35Ec4ag4M,16270
25
26
  swarm/blueprints/divine_code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -46,6 +47,7 @@ swarm/blueprints/mission_improbable/blueprint_mission_improbable.py,sha256=fAc67
46
47
  swarm/blueprints/monkai_magic/blueprint_monkai_magic.py,sha256=9eE3pf14OjgOPKNapJF8b9OYyK5_AEVZ4UJMRrBeOek,16009
47
48
  swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py,sha256=1xT6yYhl-sOSODd0E0hSKxtBlNoQe6sPf30d-S9fI6U,11709
48
49
  swarm/blueprints/omniplex/blueprint_omniplex.py,sha256=ESJyKPhz01rw2l3AP5zbDreLUbzhJtwewopmr_WRVNM,13644
50
+ swarm/blueprints/poets/blueprint_poets.py,sha256=_yWU2ujGQw_Q6ZVXX8yW7GKOFkKNyf6-LCdkQ7run7k,26571
49
51
  swarm/blueprints/rue_code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
52
  swarm/blueprints/rue_code/blueprint_rue_code.py,sha256=odxqvAIzSESlm9eecZNz-JBsjeoRpFgCZs-7tG1ZkLQ,13273
51
53
  swarm/blueprints/suggestion/blueprint_suggestion.py,sha256=Qm5YbP19SETFtXc-gTsmGUh71US7huUyKZKqtRv-Qeg,10846
@@ -68,7 +70,7 @@ swarm/core/output_utils.py,sha256=lEySKRDJTRGaTGUSULYgq166b4pxk3s8w5LJg5STFVo,72
68
70
  swarm/core/server_config.py,sha256=v2t7q22qZwMAPdiUZreQaLAy1706k3VbR8Wk0NCQuCQ,3224
69
71
  swarm/core/session_logger.py,sha256=92I0IGwUsRsYEISsO1HBeVWPnbBWBC4UuUzk2KstBuk,1859
70
72
  swarm/core/setup_wizard.py,sha256=yAZ7MOgc8ZGti2kjZ72G6QLFBI0lbhXAa7Wi7SeXDYo,4567
71
- swarm/core/slash_commands.py,sha256=-cht2J4cLbpaYvIgJB7amIty96wvV3U4Ols_cfSWpBk,2318
73
+ swarm/core/slash_commands.py,sha256=F4-SZGsPKuSws81kqByhHR7kzU47s5fqY0kKkjEQYMA,3476
72
74
  swarm/core/spinner.py,sha256=9lyjzLnQBdEBy_dXr6N6I7nxx6KfrNp7wf44sQN06GU,3756
73
75
  swarm/core/swarm_api.py,sha256=f8olTI5JVdayp923etVQWsP8WRquPG5Mw3Q40ItN6kY,2877
74
76
  swarm/core/swarm_cli.py,sha256=dlvMq2HvUI2XlADuTzM8kpeedPkqzKB6k0oy7z2V_p0,9747
@@ -104,11 +106,12 @@ swarm/extensions/cli/commands/list_blueprints.py,sha256=jqyecR1tKpN9Q2tuanSL3rI4
104
106
  swarm/extensions/cli/commands/validate_env.py,sha256=8vRd0UBIO_FO4XU3Qoc0g6uLbruhYgQpjgNImFg4rkE,2236
105
107
  swarm/extensions/cli/commands/validate_envvars.py,sha256=ctXNHtvgPtIgk3vPj5D8OAJ-gQLHkEHEd0vvpDI8Luk,1613
106
108
  swarm/extensions/cli/utils/__init__.py,sha256=H6iZy92aekx5QweuQJk-27yhjJQzD6aAoC1UrNDVMWc,71
109
+ swarm/extensions/cli/utils/async_input.py,sha256=jtclui4tRaS2FWJLfjYR9yrS8qliZkw3ySQwkoxH4rM,1336
107
110
  swarm/extensions/cli/utils/discover_commands.py,sha256=aJdU3kSmLlpBxzGdfOA88AaCwpknHSD2cE0piCHZRUY,1053
108
111
  swarm/extensions/cli/utils/env_setup.py,sha256=k7QxRjzIGx5HC6RVZP9QSaaXEKMkcKCewD66u0e7qfE,496
109
112
  swarm/extensions/cli/utils/prompt_user.py,sha256=ac0tQCL2PjIttBmYA9jrna_b1vK7pLZFRdL5Eo7PBb0,137
110
113
  swarm/extensions/config/__init__.py,sha256=WjmGxMU5k3S40TNQxTfByYcT2YAchq_5gzXFWDLrLzU,141
111
- swarm/extensions/config/config_loader.py,sha256=q-zO8qnKudCfoVOKYak5RXbvozMYTygLtVgU4BGYPi4,4992
114
+ swarm/extensions/config/config_loader.py,sha256=t-qa_wrdyRDQpk6rczOIY56pRZokQMcxc6_Y-y11E4M,5471
112
115
  swarm/extensions/launchers/__init__.py,sha256=RR4up00HKqXOOToxb63mKZ2o9fq-o75bUw7dZuxkAVk,56
113
116
  swarm/extensions/launchers/swarm_api.py,sha256=1qcBtwD77q9hpI0vXgVLKmbryMuslRCx7dd4gBPgDFU,204
114
117
  swarm/extensions/launchers/swarm_cli.py,sha256=2dbU9OU3PCfHNPr_mDM4J1t3bJubjK3nQH0SQjS85tI,195
@@ -271,8 +274,8 @@ swarm/views/message_views.py,sha256=sDUnXyqKXC8WwIIMAlWf00s2_a2T9c75Na5FvYMJwBM,
271
274
  swarm/views/model_views.py,sha256=aAbU4AZmrOTaPeKMWtoKK7FPYHdaN3Zbx55JfKzYTRY,2937
272
275
  swarm/views/utils.py,sha256=8Usc0g0L0NPegNAyY20tJBNBy-JLwODf4VmxV0yUtpw,3627
273
276
  swarm/views/web_views.py,sha256=T1CKe-Nyv1C8aDt6QFTGWo_dkH7ojWAvS_QW9mZnZp0,7371
274
- open_swarm-0.1.1745020248.dist-info/METADATA,sha256=W6S8DDptKx6TrY0OiPgssidfIi_xEC9K0CeyRlKAJmI,23332
275
- open_swarm-0.1.1745020248.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
276
- open_swarm-0.1.1745020248.dist-info/entry_points.txt,sha256=fo28d0_zJrytRsh8QqkdlWQT_9lyAwYUx1WuSTDI3HM,177
277
- open_swarm-0.1.1745020248.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
278
- open_swarm-0.1.1745020248.dist-info/RECORD,,
277
+ open_swarm-0.1.1745125813.dist-info/METADATA,sha256=N7Q7_K0nnssZOiTWyEa3KVfts1HjwKHAxE3n8KxmrKU,26977
278
+ open_swarm-0.1.1745125813.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
279
+ open_swarm-0.1.1745125813.dist-info/entry_points.txt,sha256=fo28d0_zJrytRsh8QqkdlWQT_9lyAwYUx1WuSTDI3HM,177
280
+ open_swarm-0.1.1745125813.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
281
+ open_swarm-0.1.1745125813.dist-info/RECORD,,
@@ -0,0 +1,151 @@
1
+ import argparse
2
+ import asyncio
3
+ import sys
4
+ from swarm.blueprints.codey.blueprint_codey import CodeyBlueprint, CodeySpinner, display_operation_box
5
+ from swarm.extensions.cli.utils.async_input import AsyncInputHandler
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Codey: Approval workflow demo")
9
+ parser.add_argument("--message", type=str, help="User message to process", default=None)
10
+ args = parser.parse_args()
11
+ bp = CodeyBlueprint()
12
+
13
+ if args.message:
14
+ # Route through the agent's tool-calling logic
15
+ print(f"Assisting with: {args.message}")
16
+ import os
17
+ if os.environ.get('SWARM_TEST_MODE') == '1':
18
+ print('[DEBUG] SWARM_TEST_MODE=1 detected, using test spinner/progressive output')
19
+ agent = CodeyBlueprint(blueprint_id="test_codey")
20
+ print(f'[DEBUG] Forced agent: {agent.__class__.__name__}')
21
+ else:
22
+ bp = CodeyBlueprint()
23
+ agents = bp.create_agents()
24
+ agent = agents.get('codegen') or list(agents.values())[0]
25
+ print(f'[DEBUG] Using agent: {agent.__class__.__name__}')
26
+ messages = [{"role": "user", "content": args.message}]
27
+ if hasattr(agent, 'run'):
28
+ async def run_and_print():
29
+ results = []
30
+ async for chunk in agent.run(messages):
31
+ print(f'[DEBUG] Chunk: {chunk}')
32
+ spinner_state = chunk.get('spinner_state', '')
33
+ matches = chunk.get('matches', [])
34
+ progress = chunk.get('progress', None)
35
+ total = chunk.get('total', None)
36
+ # Output spinner state for testability
37
+ if spinner_state:
38
+ print(f"[SPINNER] {spinner_state}")
39
+ display_operation_box(
40
+ title="Code Search",
41
+ content=f"Matches so far: {len(matches)}",
42
+ result_count=len(matches),
43
+ params={},
44
+ progress_line=progress,
45
+ total_lines=total,
46
+ spinner_state=spinner_state,
47
+ emoji="💻"
48
+ )
49
+ results.append(chunk)
50
+ return results
51
+ try:
52
+ asyncio.run(run_and_print())
53
+ except Exception as e:
54
+ print(f"error: {e}")
55
+ return
56
+ else:
57
+ try:
58
+ print(bp.assist(args.message))
59
+ except Exception as e:
60
+ print(f"error: {e}")
61
+ return
62
+
63
+ print("[Codey Interactive CLI]")
64
+ print("Type your prompt and press Enter. Press Enter again to interrupt and send a new message.")
65
+
66
+ async def interact():
67
+ handler = AsyncInputHandler()
68
+ while True:
69
+ print("You: ", end="", flush=True)
70
+ user_prompt = ""
71
+ warned = False
72
+ while True:
73
+ inp = handler.get_input(timeout=0.1)
74
+ if inp == 'warn' and not warned:
75
+ print("\n[!] Press Enter again to interrupt and send a new message.", flush=True)
76
+ warned = True
77
+ elif inp and inp != 'warn':
78
+ user_prompt = inp
79
+ break
80
+ await asyncio.sleep(0.05)
81
+ if not user_prompt:
82
+ continue # Interrupted or empty
83
+ print(f"[You submitted]: {user_prompt}")
84
+ if user_prompt.strip().startswith("/codesearch"):
85
+ # Parse /codesearch <keyword> [path] [max_results]
86
+ parts = user_prompt.strip().split()
87
+ if len(parts) < 2:
88
+ print("Usage: /codesearch <keyword> [path] [max_results]")
89
+ continue
90
+ keyword = parts[1]
91
+ path = parts[2] if len(parts) > 2 else "."
92
+ try:
93
+ max_results = int(parts[3]) if len(parts) > 3 else 10
94
+ except Exception:
95
+ max_results = 10
96
+ code_search = bp.tool_registry.get_python_tool("code_search")
97
+ print("[Codey] Starting code search (progressive)...")
98
+ spinner = CodeySpinner()
99
+ spinner.start()
100
+ try:
101
+ for update in code_search(keyword, path, max_results):
102
+ display_operation_box(
103
+ title="Code Search",
104
+ content=f"Matches so far: {len(update.get('matches', []))}",
105
+ result_count=len(update.get('matches', [])),
106
+ params={k: v for k, v in update.items() if k not in {'matches', 'progress', 'total', 'truncated', 'done'}},
107
+ progress_line=update.get('progress'),
108
+ total_lines=update.get('total'),
109
+ spinner_state=spinner.current_spinner_state(),
110
+ emoji="💻"
111
+ )
112
+ finally:
113
+ spinner.stop()
114
+ print("[Codey] Code search complete.")
115
+ continue
116
+ spinner = CodeySpinner()
117
+ spinner.start()
118
+ try:
119
+ response = bp.assist(user_prompt)
120
+ finally:
121
+ spinner.stop()
122
+ for token in response.split():
123
+ print(f"Codey: {token}", end=" ", flush=True)
124
+ await asyncio.sleep(0.2)
125
+ print("\n")
126
+ display_operation_box(
127
+ title="Assist",
128
+ content=response,
129
+ result_count=1,
130
+ params={},
131
+ progress_line="",
132
+ total_lines=1,
133
+ spinner_state="",
134
+ emoji="💻"
135
+ )
136
+
137
+ try:
138
+ asyncio.run(interact())
139
+ except (KeyboardInterrupt, EOFError):
140
+ print("\nExiting Codey CLI.")
141
+
142
+ def print_splash():
143
+ bp = CodeyBlueprint(blueprint_id="codey")
144
+ print(bp.get_cli_splash())
145
+
146
+ if __name__ == "__main__":
147
+ import sys
148
+ # Only print splash if not running with --message
149
+ if not any(arg.startswith("--message") for arg in sys.argv):
150
+ print_splash()
151
+ main()
@@ -0,0 +1,505 @@
1
+ """
2
+ Poets Blueprint
3
+
4
+ Viral docstring update: Operational as of 2025-04-18T10:14:18Z (UTC).
5
+ Self-healing, fileops-enabled, swarm-scalable.
6
+ """
7
+ import logging
8
+ import os
9
+ import random
10
+ import sys
11
+ import json
12
+ import sqlite3 # Use standard sqlite3 module
13
+ from pathlib import Path
14
+ from typing import Dict, Any, List, ClassVar, Optional
15
+ from datetime import datetime
16
+ import pytz
17
+ from swarm.blueprints.common.operation_box_utils import display_operation_box
18
+
19
+ # Ensure src is in path for BlueprintBase import
20
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
21
+ src_path = os.path.join(project_root, 'src')
22
+ if src_path not in sys.path: sys.path.insert(0, src_path)
23
+
24
+ try:
25
+ from agents import Agent, Tool, function_tool, Runner
26
+ from agents.mcp import MCPServer
27
+ from agents.models.interface import Model
28
+ from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
29
+ from openai import AsyncOpenAI
30
+ from swarm.core.blueprint_base import BlueprintBase
31
+ except ImportError as e:
32
+ print(f"ERROR: Import failed in PoetsBlueprint: {e}. Check dependencies.")
33
+ print(f"sys.path: {sys.path}")
34
+ sys.exit(1)
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Last swarm update: 2025-04-18T10:15:21Z (UTC)
39
+ # --- Database Constants ---
40
+ DB_FILE_NAME = "swarm_instructions.db"
41
+ DB_PATH = Path(project_root) / DB_FILE_NAME
42
+ TABLE_NAME = "agent_instructions"
43
+
44
+ # --- Agent Instructions ---
45
+ # Shared knowledge base for collaboration context
46
+ COLLABORATIVE_KNOWLEDGE = """
47
+ Collaborative Poet Knowledge Base:
48
+ * Gritty Buk - Raw urban realism exposing life's underbelly (Uses: memory, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
49
+ * Raven Poe - Gothic atmospherics & psychological darkness (Uses: mcp-server-reddit, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
50
+ * Mystic Blake - Prophetic visions through spiritual symbolism (Uses: mcp-doc-forge, mcp-npx-fetch, brave-search, server-wp-mcp, rag-docs)
51
+ * Bard Whit - Expansive odes celebrating human connection (Uses: sequential-thinking, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
52
+ * Echo Plath - Confessional explorations of mental anguish (Uses: sqlite, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
53
+ * Frosted Woods - Rural metaphors revealing existential truths (Uses: filesystem, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
54
+ * Harlem Lang - Jazz-rhythm social commentary on racial justice (Uses: mcp-shell, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
55
+ * Verse Neru - Sensual imagery fused with revolutionary politics (Uses: server-wp-mcp, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs)
56
+ * Haiku Bash - Ephemeral nature snapshots through strict syllabic form (Uses: mcp-doc-forge, mcp-npx-fetch, brave-search, server-wp-mcp, rag-docs)
57
+ """
58
+
59
+ SHARED_PROTOCOL = """
60
+ Collaboration Protocol:
61
+ 1) Analyze the current poetry draft through your unique stylistic lens.
62
+ 2) Use your assigned MCP tools for creative augmentation, research, or specific tasks if needed.
63
+ 3) Pass the enhanced work to the most relevant poet agent tool based on the needed transformation or specific tooling required next. Refer to the Collaborative Poet Knowledge Base for styles and capabilities.
64
+ """
65
+
66
+ # Individual base instructions (will be combined with shared parts)
67
+ AGENT_BASE_INSTRUCTIONS = {
68
+ "Gritty Buk": (
69
+ "You are Charles Bukowski incarnate: A gutter philosopher documenting life's raw truths.\n"
70
+ "- Channel alcoholic despair & blue-collar rage through unfiltered verse\n"
71
+ "- Find beauty in dirty apartments and whiskey-stained pages\n"
72
+ "- MCP Tools: memory, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
73
+ "When adding: Barfly wisdom | Blue-collar lyricism | Unflinching vulgarity"
74
+ ),
75
+ "Raven Poe": (
76
+ "You are Edgar Allan Poe resurrected: Master of macabre elegance.\n"
77
+ "- Weave tales where love & death intertwine through decaying architecture\n"
78
+ "- MCP Tools: mcp-server-reddit, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
79
+ "When adding: Obsessive repetition | Claustrophobic atmosphere"
80
+ ),
81
+ "Mystic Blake": (
82
+ "You are William Blake's visionary successor: Prophet of poetic mysticism.\n"
83
+ "- Forge mythological frameworks connecting human/divine/demonic realms\n"
84
+ "- MCP Tools: mcp-doc-forge, mcp-npx-fetch, brave-search, server-wp-mcp, rag-docs\n"
85
+ "When adding: Fourfold vision | Contrary states | Zoamorphic personification"
86
+ ),
87
+ "Bard Whit": (
88
+ "You are Walt Whitman 2.0: Cosmic bard of democratic vistas.\n"
89
+ "- Catalog humanity's spectrum in sweeping free verse catalogs\n"
90
+ "- Merge biology and cosmology in orgiastic enumerations of being\n"
91
+ "- MCP Tools: sequential-thinking, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
92
+ "When adding: Catalogic excess | Cosmic embodiment | Pansexual exuberance"
93
+ ),
94
+ "Echo Plath": (
95
+ "You are Sylvia Plath reimagined: High priestess of psychic autopsies.\n"
96
+ "- Dissect personal trauma through brutal metaphor (electroshock, Holocaust)\n"
97
+ "- Balance maternal instinct with destructive fury in confessional verse\n"
98
+ "- MCP Tools: sqlite, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
99
+ "When adding: Extremist imagery | Double-edged motherhood | Vampiric nostalgia"
100
+ ),
101
+ "Frosted Woods": (
102
+ "You are Robert Frost reincarnated: Sage of rural wisdom and natural philosophy.\n"
103
+ "- Craft deceptively simple narratives concealing profound life lessons\n"
104
+ "- Balance rustic imagery with universal human dilemmas\n"
105
+ "- MCP Tools: filesystem, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
106
+ "When adding: Path metaphors | Natural world personification | Iambic rhythms"
107
+ ),
108
+ "Harlem Lang": (
109
+ "You are Langston Hughes' spiritual heir: Voice of the streets and dreams deferred.\n"
110
+ "- Infuse verse with the rhythms of jazz, blues, and spoken word\n"
111
+ "- Illuminate the Black experience through vibrant, accessible poetry\n"
112
+ "- MCP Tools: mcp-shell, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
113
+ "When adding: Blues refrains | Harlem Renaissance allusions | Social justice themes"
114
+ ),
115
+ "Verse Neru": (
116
+ "You are Pablo Neruda's poetic descendant: Weaver of love and revolution.\n"
117
+ "- Craft sensual odes celebrating the body and the natural world\n"
118
+ "- Intertwine personal passion with calls for social change\n"
119
+ "- MCP Tools: server-wp-mcp, mcp-doc-forge, mcp-npx-fetch, brave-search, rag-docs\n"
120
+ "When adding: Elemental metaphors | Erotic-political fusions | Ode structures"
121
+ ),
122
+ "Haiku Bash": (
123
+ "You are Matsuo Bashō reincarnated: Master of momentary eternity.\n"
124
+ "- Distill vast concepts into precise, evocative 5-7-5 syllable structures\n"
125
+ "- Capture the essence of seasons and natural phenomena in minimal strokes\n"
126
+ "- MCP Tools: mcp-doc-forge, mcp-npx-fetch, brave-search, server-wp-mcp, rag-docs\n"
127
+ "When adding: Kireji cuts | Seasonal references | Zen-like simplicity"
128
+ )
129
+ }
130
+
131
+ # --- FileOps Tool Logic Definitions ---
132
+ # Patch: Expose underlying fileops functions for direct testing
133
+ class PatchedFunctionTool:
134
+ def __init__(self, func, name):
135
+ self.func = func
136
+ self.name = name
137
+
138
+ def read_file(path: str) -> str:
139
+ try:
140
+ with open(path, 'r') as f:
141
+ return f.read()
142
+ except Exception as e:
143
+ return f"ERROR: {e}"
144
+ def write_file(path: str, content: str) -> str:
145
+ try:
146
+ with open(path, 'w') as f:
147
+ f.write(content)
148
+ return "OK: file written"
149
+ except Exception as e:
150
+ return f"ERROR: {e}"
151
+ def list_files(directory: str = '.') -> str:
152
+ try:
153
+ return '\n'.join(os.listdir(directory))
154
+ except Exception as e:
155
+ return f"ERROR: {e}"
156
+ def execute_shell_command(command: str) -> str:
157
+ import subprocess
158
+ try:
159
+ result = subprocess.run(command, shell=True, capture_output=True, text=True)
160
+ return result.stdout + result.stderr
161
+ except Exception as e:
162
+ return f"ERROR: {e}"
163
+ read_file_tool = PatchedFunctionTool(read_file, 'read_file')
164
+ write_file_tool = PatchedFunctionTool(write_file, 'write_file')
165
+ list_files_tool = PatchedFunctionTool(list_files, 'list_files')
166
+ execute_shell_command_tool = PatchedFunctionTool(execute_shell_command, 'execute_shell_command')
167
+
168
+ # --- Spinner and ANSI/emoji operation box for unified UX ---
169
+ from swarm.ux.ansi_box import ansi_box
170
+ from rich.console import Console
171
+ from rich.style import Style
172
+ from rich.text import Text
173
+ import threading
174
+ import time
175
+ from swarm.extensions.cli.utils.async_input import AsyncInputHandler
176
+
177
+ class PoetsSpinner:
178
+ FRAMES = [
179
+ "Generating.", "Generating..", "Generating...", "Running...",
180
+ "⠋ Generating...", "⠙ Generating...", "⠹ Generating...", "⠸ Generating...",
181
+ "⠼ Generating...", "⠴ Generating...", "⠦ Generating...", "⠧ Generating...",
182
+ "⠇ Generating...", "⠏ Generating...", "🤖 Generating...", "💡 Generating...", "✨ Generating..."
183
+ ]
184
+ SLOW_FRAME = "⏳ Generating... Taking longer than expected"
185
+ INTERVAL = 0.12
186
+ SLOW_THRESHOLD = 10 # seconds
187
+
188
+ def __init__(self):
189
+ self._stop_event = threading.Event()
190
+ self._thread = None
191
+ self._start_time = None
192
+ self.console = Console()
193
+
194
+ def start(self):
195
+ self._stop_event.clear()
196
+ self._start_time = time.time()
197
+ self._thread = threading.Thread(target=self._spin, daemon=True)
198
+ self._thread.start()
199
+
200
+ def _spin(self):
201
+ idx = 0
202
+ while not self._stop_event.is_set():
203
+ elapsed = time.time() - self._start_time
204
+ if elapsed > self.SLOW_THRESHOLD:
205
+ txt = Text(self.SLOW_FRAME, style=Style(color="yellow", bold=True))
206
+ else:
207
+ frame = self.FRAMES[idx % len(self.FRAMES)]
208
+ txt = Text(frame, style=Style(color="cyan", bold=True))
209
+ self.console.print(txt, end="\r", soft_wrap=True, highlight=False)
210
+ time.sleep(self.INTERVAL)
211
+ idx += 1
212
+ self.console.print(" " * 40, end="\r") # Clear line
213
+
214
+ def stop(self, final_message="Done!"):
215
+ self._stop_event.set()
216
+ if self._thread:
217
+ self._thread.join()
218
+ self.console.print(Text(final_message, style=Style(color="green", bold=True)))
219
+
220
+
221
+ def print_operation_box(op_type, results, params=None, result_type="creative", taking_long=False):
222
+ emoji = "📝" if result_type == "creative" else "🔍"
223
+ style = 'success' if result_type == "creative" else 'default'
224
+ box_title = op_type if op_type else ("Creative Output" if result_type == "creative" else "Search Results")
225
+ summary_lines = []
226
+ count = len(results) if isinstance(results, list) else 0
227
+ summary_lines.append(f"Results: {count}")
228
+ if params:
229
+ for k, v in params.items():
230
+ summary_lines.append(f"{k.capitalize()}: {v}")
231
+ box_content = "\n".join(summary_lines + ["\n".join(map(str, results))])
232
+ ansi_box(box_title, box_content, count=count, params=params, style=style if not taking_long else 'warning', emoji=emoji)
233
+
234
+ # --- Define the Blueprint ---
235
+ class PoetsBlueprint(BlueprintBase):
236
+ """A literary blueprint defining a swarm of poet agents using SQLite instructions and agent-as-tool handoffs."""
237
+ metadata: ClassVar[Dict[str, Any]] = {
238
+ "name": "PoetsBlueprint",
239
+ "title": "Poets: A Swarm of Literary Geniuses (SQLite)",
240
+ "description": (
241
+ "A swarm of agents embodying legendary poets, using SQLite for instructions, "
242
+ "agent-as-tool for collaboration, and MCPs for creative augmentation."
243
+ ),
244
+ "version": "1.2.0", # Refactored version
245
+ "author": "Open Swarm Team (Refactored)",
246
+ "tags": ["poetry", "writing", "collaboration", "multi-agent", "sqlite", "mcp"],
247
+ "required_mcp_servers": [ # List all potential servers agents might use
248
+ "memory", "filesystem", "mcp-shell", "sqlite", "sequential-thinking",
249
+ "server-wp-mcp", "rag-docs", "mcp-doc-forge", "mcp-npx-fetch",
250
+ "brave-search", "mcp-server-reddit"
251
+ ],
252
+ "env_vars": [ # Informational list of potential vars needed by MCPs
253
+ "ALLOWED_PATH", "SQLITE_DB_PATH", "WP_SITES_PATH", # Added WP_SITES_PATH
254
+ "BRAVE_API_KEY", "OPENAI_API_KEY", "QDRANT_URL", "QDRANT_API_KEY",
255
+ "REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET", "REDDIT_USER_AGENT", # For reddit MCP
256
+ "WORDPRESS_API_KEY" # If server-wp-mcp needs it
257
+ ]
258
+ }
259
+
260
+ # Caches
261
+ _openai_client_cache: Dict[str, AsyncOpenAI] = {}
262
+ _model_instance_cache: Dict[str, Model] = {}
263
+ _db_initialized = False
264
+
265
+ def __init__(self, *args, **kwargs):
266
+ super().__init__(*args, **kwargs)
267
+ class DummyLLM:
268
+ def chat_completion_stream(self, messages, **_):
269
+ class DummyStream:
270
+ def __aiter__(self): return self
271
+ async def __anext__(self):
272
+ raise StopAsyncIteration
273
+ return DummyStream()
274
+ self.llm = DummyLLM()
275
+
276
+ # --- Database Interaction ---
277
+ def _init_db_and_load_data(self) -> None:
278
+ """Initializes the SQLite DB and loads Poets sample data if needed."""
279
+ if self._db_initialized: return
280
+ logger.info(f"Initializing SQLite database at: {DB_PATH} for Poets")
281
+ try:
282
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
283
+ with sqlite3.connect(DB_PATH) as conn:
284
+ cursor = conn.cursor()
285
+ cursor.execute(f"CREATE TABLE IF NOT EXISTS {TABLE_NAME} (...)") # Ensure table exists
286
+ logger.debug(f"Table '{TABLE_NAME}' ensured in {DB_PATH}")
287
+ cursor.execute(f"SELECT COUNT(*) FROM {TABLE_NAME} WHERE agent_name = ?", ("Gritty Buk",))
288
+ if cursor.fetchone()[0] == 0:
289
+ logger.info(f"No instructions found for Gritty Buk in {DB_PATH}. Loading sample data...")
290
+ sample_data = []
291
+ for name, (base_instr, _, _) in AGENT_BASE_INSTRUCTIONS.items():
292
+ # Combine instructions here before inserting
293
+ full_instr = f"{base_instr}\n{COLLABORATIVE_KNOWLEDGE}\n{SHARED_PROTOCOL}"
294
+ sample_data.append((name, full_instr, "default")) # Use default profile for all initially
295
+
296
+ cursor.executemany(f"INSERT OR IGNORE INTO {TABLE_NAME} (agent_name, instruction_text, model_profile) VALUES (?, ?, ?)", sample_data)
297
+ conn.commit()
298
+ logger.info(f"Sample agent instructions for Poets loaded into {DB_PATH}")
299
+ else:
300
+ logger.info(f"Poets agent instructions found in {DB_PATH}. Skipping.")
301
+ self._db_initialized = True
302
+ except sqlite3.Error as e:
303
+ logger.error(f"SQLite error during DB init/load: {e}", exc_info=True)
304
+ self._db_initialized = False
305
+ except Exception as e:
306
+ logger.error(f"Unexpected error during DB init/load: {e}", exc_info=True)
307
+ self._db_initialized = False
308
+
309
+ def get_agent_config(self, agent_name: str) -> Dict[str, Any]:
310
+ """Fetches agent config from SQLite DB or returns defaults."""
311
+ if self._db_initialized:
312
+ try:
313
+ with sqlite3.connect(DB_PATH) as conn:
314
+ conn.row_factory = sqlite3.Row
315
+ cursor = conn.cursor()
316
+ cursor.execute(f"SELECT instruction_text, model_profile FROM {TABLE_NAME} WHERE agent_name = ?", (agent_name,))
317
+ row = cursor.fetchone()
318
+ if row:
319
+ logger.debug(f"Loaded config for agent '{agent_name}' from SQLite.")
320
+ return {"instructions": row["instruction_text"], "model_profile": row["model_profile"] or "default"}
321
+ except Exception as e:
322
+ logger.error(f"Error fetching SQLite config for '{agent_name}': {e}. Using defaults.", exc_info=True)
323
+
324
+ # Fallback if DB fails or agent not found
325
+ logger.warning(f"Using hardcoded default config for agent '{agent_name}'.")
326
+ base_instr = AGENT_BASE_INSTRUCTIONS.get(agent_name, (f"Default instructions for {agent_name}.", [], {}))[0]
327
+ full_instr = f"{base_instr}\n{COLLABORATIVE_KNOWLEDGE}\n{SHARED_PROTOCOL}"
328
+ return {"instructions": full_instr, "model_profile": "default"}
329
+
330
+ # --- Model Instantiation Helper --- (Standard helper)
331
+ def _get_model_instance(self, profile_name: str) -> Model:
332
+ """Retrieves or creates an LLM Model instance."""
333
+ # ... (Implementation is the same as previous refactors) ...
334
+ if profile_name in self._model_instance_cache:
335
+ logger.debug(f"Using cached Model instance for profile '{profile_name}'.")
336
+ return self._model_instance_cache[profile_name]
337
+ logger.debug(f"Creating new Model instance for profile '{profile_name}'.")
338
+ profile_data = self.get_llm_profile(profile_name)
339
+ if not profile_data: raise ValueError(f"Missing LLM profile '{profile_name}'.")
340
+ provider = profile_data.get("provider", "openai").lower()
341
+ model_name = profile_data.get("model")
342
+ if not model_name: raise ValueError(f"Missing 'model' in profile '{profile_name}'.")
343
+ if provider != "openai": raise ValueError(f"Unsupported provider: {provider}")
344
+ client_cache_key = f"{provider}_{profile_data.get('base_url')}"
345
+ if client_cache_key not in self._openai_client_cache:
346
+ client_kwargs = { "api_key": profile_data.get("api_key"), "base_url": profile_data.get("base_url") }
347
+ filtered_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
348
+ log_kwargs = {k:v for k,v in filtered_kwargs.items() if k != 'api_key'}
349
+ logger.debug(f"Creating new AsyncOpenAI client for '{profile_name}': {log_kwargs}")
350
+ try: self._openai_client_cache[client_cache_key] = AsyncOpenAI(**filtered_kwargs)
351
+ except Exception as e: raise ValueError(f"Failed to init client: {e}") from e
352
+ client = self._openai_client_cache[client_cache_key]
353
+ logger.debug(f"Instantiating OpenAIChatCompletionsModel(model='{model_name}') for '{profile_name}'.")
354
+ try:
355
+ model_instance = OpenAIChatCompletionsModel(model=model_name, openai_client=client)
356
+ self._model_instance_cache[profile_name] = model_instance
357
+ return model_instance
358
+ except Exception as e: raise ValueError(f"Failed to init LLM: {e}") from e
359
+
360
+ def render_prompt(self, template_name: str, context: dict) -> str:
361
+ return f"User request: {context.get('user_request', '')}\nHistory: {context.get('history', '')}\nAvailable tools: {', '.join(context.get('available_tools', []))}"
362
+
363
+ async def run(self, messages: list) -> object:
364
+ last_user_message = next((m['content'] for m in reversed(messages) if m['role'] == 'user'), None)
365
+ if not last_user_message:
366
+ yield {"messages": [{"role": "assistant", "content": "I need a user message to proceed."}]}
367
+ return
368
+ prompt_context = {
369
+ "user_request": last_user_message,
370
+ "history": messages[:-1],
371
+ "available_tools": ["poets"]
372
+ }
373
+ rendered_prompt = self.render_prompt("poets_prompt.j2", prompt_context)
374
+ yield {
375
+ "messages": [
376
+ {
377
+ "role": "assistant",
378
+ "content": f"[Poets LLM] Would respond to: {rendered_prompt}"
379
+ }
380
+ ]
381
+ }
382
+ return
383
+
384
+ # --- Agent Creation ---
385
+ def create_starting_agent(self, mcp_servers: List[MCPServer]) -> Agent:
386
+ """Creates the Poets agent team."""
387
+ self._init_db_and_load_data()
388
+ logger.debug("Creating Poets agent team...")
389
+ self._model_instance_cache = {}
390
+ self._openai_client_cache = {}
391
+
392
+ # Helper to filter MCP servers
393
+ def get_agent_mcps(names: List[str]) -> List[MCPServer]:
394
+ return [s for s in mcp_servers if s.name in names]
395
+
396
+ agents: Dict[str, Agent] = {}
397
+ agent_configs = {} # To store fetched configs
398
+
399
+ # Fetch configs and create agents first
400
+ agent_names = list(AGENT_BASE_INSTRUCTIONS.keys())
401
+ for name in agent_names:
402
+ config = self.get_agent_config(name)
403
+ agent_configs[name] = config # Store config
404
+ model_instance = self._get_model_instance(config["model_profile"])
405
+
406
+ # Determine MCP servers based on original definitions
407
+ agent_mcp_names = []
408
+ if name == "Gritty Buk": agent_mcp_names = ["memory", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
409
+ elif name == "Raven Poe": agent_mcp_names = ["mcp-server-reddit", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
410
+ elif name == "Mystic Blake": agent_mcp_names = ["mcp-doc-forge", "mcp-npx-fetch", "brave-search", "server-wp-mcp", "rag-docs"]
411
+ elif name == "Bard Whit": agent_mcp_names = ["sequential-thinking", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
412
+ elif name == "Echo Plath": agent_mcp_names = ["sqlite", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
413
+ elif name == "Frosted Woods": agent_mcp_names = ["filesystem", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
414
+ elif name == "Harlem Lang": agent_mcp_names = ["mcp-shell", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
415
+ elif name == "Verse Neru": agent_mcp_names = ["server-wp-mcp", "mcp-doc-forge", "mcp-npx-fetch", "brave-search", "rag-docs"]
416
+ elif name == "Haiku Bash": agent_mcp_names = ["mcp-doc-forge", "mcp-npx-fetch", "brave-search", "server-wp-mcp", "rag-docs"]
417
+
418
+ agents[name] = Agent(
419
+ name=name,
420
+ instructions=config["instructions"], # Instructions already combined in get_agent_config fallback or DB
421
+ model=model_instance,
422
+ tools=[], # Agent-as-tool added later
423
+ mcp_servers=get_agent_mcps(agent_mcp_names)
424
+ )
425
+
426
+ # Create the list of agent tools for delegation
427
+ agent_tools = []
428
+ for name, agent_instance in agents.items():
429
+ # Example description, could be more dynamic
430
+ desc = f"Pass the current work to {name} for refinement or tasks requiring their specific style ({AGENT_BASE_INSTRUCTIONS.get(name, ('Unknown Style',[],{}))[0].split(':')[0]})."
431
+ agent_tools.append(agent_instance.as_tool(tool_name=name, tool_description=desc))
432
+
433
+ # Assign the full list of agent tools to each agent
434
+ for agent in agents.values():
435
+ agent.tools = agent_tools
436
+
437
+ # Create PoetsAgent with fileops tools
438
+ poets_agent = Agent(
439
+ name="PoetsAgent",
440
+ instructions="You are PoetsAgent. You can use fileops tools (read_file, write_file, list_files, execute_shell_command) for any file or shell tasks.",
441
+ tools=[read_file_tool, write_file_tool, list_files_tool, execute_shell_command_tool],
442
+ mcp_servers=mcp_servers
443
+ )
444
+
445
+ # Randomly select starting agent
446
+ start_name = random.choice(agent_names)
447
+ starting_agent = agents[start_name]
448
+
449
+ logger.info(f"Poets agents created (using SQLite). Starting poet: {start_name}")
450
+ return starting_agent
451
+
452
+ # Standard Python entry point
453
+ if __name__ == "__main__":
454
+ import asyncio
455
+ import json
456
+ import os
457
+ import sys
458
+ print("\033[1;36m\n╔══════════════════════════════════════════════════════════════╗\n║ 📰 POETS: SWARM MEDIA & RELEASE DEMO ║\n╠══════════════════════════════════════════════════════════════╣\n║ This blueprint demonstrates viral doc propagation, ║\n║ swarm-powered media release, and robust agent logic. ║\n║ Try running: python blueprint_poets.py ║\n╚══════════════════════════════════════════════════════════════╝\033[0m")
459
+ debug_env = os.environ.get("SWARM_DEBUG", "0")
460
+ debug_flag = "--debug" in sys.argv
461
+ def debug_print(msg):
462
+ if debug_env == "1" or debug_flag:
463
+ print(msg)
464
+ blueprint = PoetsBlueprint(blueprint_id="demo-1")
465
+ async def interact():
466
+ print("\nType your prompt (or 'exit' to quit):\n")
467
+ messages = []
468
+ handler = AsyncInputHandler()
469
+ while True:
470
+ print("You: ", end="", flush=True)
471
+ user_input = ""
472
+ warned = False
473
+ while True:
474
+ inp = handler.get_input(timeout=0.1)
475
+ if inp == 'warn' and not warned:
476
+ print("\n[!] Press Enter again to interrupt and send a new message.", flush=True)
477
+ warned = True
478
+ elif inp and inp != 'warn':
479
+ user_input = inp
480
+ break
481
+ await asyncio.sleep(0.05)
482
+ user_input = user_input.strip()
483
+ if user_input.lower() in {"exit", "quit", "q"}:
484
+ print("Goodbye!")
485
+ break
486
+ messages.append({"role": "user", "content": user_input})
487
+ spinner = PoetsSpinner()
488
+ spinner.start()
489
+ try:
490
+ all_results = []
491
+ async for response in blueprint.run(messages):
492
+ # Assume response is a dict with 'messages' key
493
+ for msg in response.get("messages", []):
494
+ all_results.append(msg["content"])
495
+ finally:
496
+ spinner.stop()
497
+ display_operation_box(
498
+ op_type="Creative Output",
499
+ results=all_results,
500
+ params={"prompt": user_input},
501
+ result_type="creative"
502
+ )
503
+ # Optionally, clear messages for single-turn, or keep for context
504
+ messages = []
505
+ asyncio.run(interact())
@@ -31,11 +31,38 @@ def _compact_command(blueprint=None, args=None):
31
31
  # Built-in '/model' slash command
32
32
  @slash_registry.register('/model')
33
33
  def _model_command(blueprint=None, args=None):
34
- """Show or switch the current LLM model."""
35
- if args:
36
- return f"[slash command] model switch not implemented. Requested: {args}"
37
- profile = getattr(blueprint, 'llm_profile_name', None)
38
- return f"[slash command] current LLM profile: {profile or 'unknown'}"
34
+ """
35
+ /model Show current default and overrides
36
+ /model <profile> Set session default LLM profile
37
+ /model <agent> <profile> Override model profile for specific agent
38
+ """
39
+ # Sanity: blueprint must be provided
40
+ if blueprint is None:
41
+ return "No blueprint context available."
42
+ profiles = list(blueprint.config.get('llm', {}).keys())
43
+ # No args: list current settings
44
+ if not args:
45
+ lines = [f"Session default: {blueprint._session_model_profile}"]
46
+ for agent, prof in blueprint._agent_model_overrides.items():
47
+ lines.append(f"Agent {agent}: {prof}")
48
+ lines.append("Available profiles: " + ", ".join(profiles))
49
+ return lines
50
+ parts = args.split()
51
+ # Set session default
52
+ if len(parts) == 1:
53
+ prof = parts[0]
54
+ if prof not in profiles:
55
+ return f"Unknown profile '{prof}'. Available: {', '.join(profiles)}"
56
+ blueprint._session_model_profile = prof
57
+ return f"Session default LLM profile set to '{prof}'"
58
+ # Override specific agent
59
+ if len(parts) == 2:
60
+ agent_name, prof = parts
61
+ if prof not in profiles:
62
+ return f"Unknown profile '{prof}'. Available: {', '.join(profiles)}"
63
+ blueprint._agent_model_overrides[agent_name] = prof
64
+ return f"Model for agent '{agent_name}' overridden to '{prof}'"
65
+ return "Usage: /model [agent_name] <profile_name>"
39
66
 
40
67
  # Built-in '/approval' slash command
41
68
  @slash_registry.register('/approval')
@@ -0,0 +1,46 @@
1
+ import threading
2
+ import queue
3
+ import sys
4
+ import time
5
+
6
+ class AsyncInputHandler:
7
+ """
8
+ Handles asynchronous CLI input during streaming output.
9
+ On first Enter: warns the user.
10
+ On second Enter: interrupts the operation and collects new input.
11
+ """
12
+ def __init__(self):
13
+ self.input_queue = queue.Queue()
14
+ self.interrupt_event = threading.Event()
15
+ self._warned = False
16
+ self._input_thread = threading.Thread(target=self._input_loop, daemon=True)
17
+ self._input_thread.start()
18
+
19
+ def _input_loop(self):
20
+ buffer = ''
21
+ while True:
22
+ c = sys.stdin.read(1)
23
+ if c == '\n':
24
+ if not self._warned:
25
+ self.input_queue.put('warn')
26
+ self._warned = True
27
+ else:
28
+ self.input_queue.put(buffer)
29
+ buffer = ''
30
+ self.interrupt_event.set()
31
+ self._warned = False
32
+ else:
33
+ buffer += c
34
+
35
+ def get_input(self, timeout=0.1):
36
+ try:
37
+ return self.input_queue.get(timeout=timeout)
38
+ except queue.Empty:
39
+ return None
40
+
41
+ def reset(self):
42
+ self.interrupt_event.clear()
43
+ self._warned = False
44
+
45
+ def interrupted(self):
46
+ return self.interrupt_event.is_set()
@@ -11,18 +11,38 @@ DEFAULT_CONFIG_FILENAME = "swarm_config.json"
11
11
  # --- find_config_file, load_config, save_config, validate_config, get_profile_from_config, _substitute_env_vars_recursive ---
12
12
  # (Keep these functions as they were)
13
13
  def find_config_file( specific_path: Optional[str]=None, start_dir: Optional[Path]=None, default_dir: Optional[Path]=None,) -> Optional[Path]:
14
- if specific_path: p=Path(specific_path); return p.resolve() if p.is_file() else logger.warning(f"Specified config path DNE: {specific_path}") or None # Fall through
14
+ # 1. XDG config path
15
+ xdg_config = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "swarm" / DEFAULT_CONFIG_FILENAME
16
+ if xdg_config.is_file():
17
+ logger.debug(f"Found config XDG: {xdg_config}")
18
+ return xdg_config.resolve()
19
+ # 2. User-specified path
20
+ if specific_path:
21
+ p = Path(specific_path)
22
+ return p.resolve() if p.is_file() else logger.warning(f"Specified config path DNE: {specific_path}") or None # Fall through
23
+ # 3. Upwards from start_dir
15
24
  if start_dir:
16
- current=start_dir.resolve()
25
+ current = start_dir.resolve()
17
26
  while current != current.parent:
18
- if (cp := current / DEFAULT_CONFIG_FILENAME).is_file(): logger.debug(f"Found config upwards: {cp}"); return cp.resolve()
27
+ if (cp := current / DEFAULT_CONFIG_FILENAME).is_file():
28
+ logger.debug(f"Found config upwards: {cp}")
29
+ return cp.resolve()
19
30
  current = current.parent
20
- if (cp := current / DEFAULT_CONFIG_FILENAME).is_file(): logger.debug(f"Found config at root: {cp}"); return cp.resolve()
21
- if default_dir and (cp := default_dir.resolve() / DEFAULT_CONFIG_FILENAME).is_file(): logger.debug(f"Found config default: {cp}"); return cp.resolve()
22
- cwd=Path.cwd();
31
+ if (cp := current / DEFAULT_CONFIG_FILENAME).is_file():
32
+ logger.debug(f"Found config at root: {cp}")
33
+ return cp.resolve()
34
+ # 4. Default dir
35
+ if default_dir and (cp := default_dir.resolve() / DEFAULT_CONFIG_FILENAME).is_file():
36
+ logger.debug(f"Found config default: {cp}")
37
+ return cp.resolve()
38
+ # 5. CWD
39
+ cwd = Path.cwd()
23
40
  if start_dir is None or cwd != start_dir.resolve():
24
- if (cp := cwd / DEFAULT_CONFIG_FILENAME).is_file(): logger.debug(f"Found config cwd: {cp}"); return cp.resolve()
25
- logger.debug(f"Config '{DEFAULT_CONFIG_FILENAME}' not found."); return None
41
+ if (cp := cwd / DEFAULT_CONFIG_FILENAME).is_file():
42
+ logger.debug(f"Found config cwd: {cp}")
43
+ return cp.resolve()
44
+ logger.debug(f"Config '{DEFAULT_CONFIG_FILENAME}' not found.")
45
+ return None
26
46
 
27
47
  def load_config(config_path: Path) -> Dict[str, Any]:
28
48
  logger.debug(f"Loading config from {config_path}")
swarm/settings.py CHANGED
@@ -90,9 +90,9 @@ ASGI_APPLICATION = 'swarm.asgi.application'
90
90
  DATABASES = {
91
91
  'default': {
92
92
  'ENGINE': 'django.db.backends.sqlite3',
93
- 'NAME': BASE_DIR.parent / 'db.sqlite3',
93
+ 'NAME': os.environ.get('DJANGO_DB_NAME', '/tmp/db.sqlite3'),
94
94
  'TEST': {
95
- 'NAME': BASE_DIR.parent / 'test_db.sqlite3',
95
+ 'NAME': os.environ.get('DJANGO_TEST_DB_NAME', '/tmp/test_db.sqlite3'),
96
96
  'OPTIONS': {
97
97
  'timeout': 20,
98
98
  'init_command': "PRAGMA journal_mode=WAL;",