soorma-core 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,717 @@
1
+ """
2
+ soorma init - Scaffold a new agent project.
3
+ """
4
+
5
+ import typer
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from enum import Enum
9
+
10
+
11
+ class AgentType(str, Enum):
12
+ """Type of agent to generate."""
13
+ planner = "planner"
14
+ worker = "worker"
15
+ tool = "tool"
16
+
17
+
18
+ # =============================================================================
19
+ # PLANNER TEMPLATE
20
+ # =============================================================================
21
+ PLANNER_PY_TEMPLATE = '''"""
22
+ {name} - A Soorma Planner Agent.
23
+
24
+ Planners are the "brain" of the DisCo architecture. They:
25
+ 1. Receive high-level goals from clients
26
+ 2. Decompose goals into actionable tasks
27
+ 3. Assign tasks to Worker agents based on capabilities
28
+ 4. Monitor plan execution progress
29
+
30
+ The PlatformContext provides access to:
31
+ - context.registry: Find available workers by capability
32
+ - context.memory: Store/retrieve planning context
33
+ - context.bus: Publish tasks as action-requests
34
+ - context.tracker: Monitor plan execution
35
+ """
36
+
37
+ from soorma import Planner, PlatformContext
38
+ from soorma.agents.planner import Goal, Plan, Task
39
+
40
+
41
+ # Create a Planner agent
42
+ planner = Planner(
43
+ name="{name}",
44
+ description="{description}",
45
+ version="0.1.0",
46
+ capabilities=["example_planning"], # Add your planning capabilities here
47
+ )
48
+
49
+
50
+ @planner.on_startup
51
+ async def startup():
52
+ """Called when the planner connects to the platform."""
53
+ print(f"🧠 {{planner.name}} is starting up...")
54
+ print(f" Ready to decompose goals into tasks")
55
+
56
+
57
+ @planner.on_shutdown
58
+ async def shutdown():
59
+ """Called when the planner is shutting down."""
60
+ print(f"👋 {{planner.name}} is shutting down...")
61
+
62
+
63
+ @planner.on_goal("example.goal")
64
+ async def plan_example_goal(goal: Goal, context: PlatformContext) -> Plan:
65
+ """
66
+ Handle incoming example.goal requests.
67
+
68
+ Replace this with your own goal handlers. Each goal handler:
69
+ 1. Receives a Goal with the high-level objective
70
+ 2. Has access to PlatformContext for service discovery
71
+ 3. Returns a Plan with ordered Tasks
72
+
73
+ The platform automatically:
74
+ - Tracks plan execution via Tracker service
75
+ - Publishes tasks as action-requests
76
+ - Monitors task completion and dependencies
77
+ """
78
+ print(f"Planning goal: {{goal.goal_type}} ({{goal.goal_id}})")
79
+
80
+ # Discover available workers (example)
81
+ # workers = await context.registry.find_all("data_processing")
82
+
83
+ # Decompose goal into tasks
84
+ # In real scenarios, you might use an LLM for intelligent decomposition
85
+ tasks = [
86
+ Task(
87
+ name="step_1",
88
+ assigned_to="example_task", # Worker capability
89
+ data={{"input": goal.data.get("input", "default")}},
90
+ ),
91
+ Task(
92
+ name="step_2",
93
+ assigned_to="example_task",
94
+ data={{"previous_step": "step_1"}},
95
+ depends_on=["step_1"], # Wait for step_1 to complete
96
+ ),
97
+ ]
98
+
99
+ return Plan(
100
+ goal=goal,
101
+ tasks=tasks,
102
+ metadata={{"planned_by": "{name}"}},
103
+ )
104
+
105
+
106
+ if __name__ == "__main__":
107
+ planner.run()
108
+ '''
109
+
110
+ # =============================================================================
111
+ # WORKER TEMPLATE
112
+ # =============================================================================
113
+ WORKER_PY_TEMPLATE = '''"""
114
+ {name} - A Soorma Worker Agent.
115
+
116
+ Workers are the "hands" of the DisCo architecture. They:
117
+ 1. Register capabilities with the Registry
118
+ 2. Subscribe to action-requests matching their capabilities
119
+ 3. Execute tasks with domain expertise (often using LLMs)
120
+ 4. Report progress and results
121
+
122
+ The PlatformContext provides access to:
123
+ - context.registry: Service discovery & capabilities
124
+ - context.memory: Distributed state management
125
+ - context.bus: Event choreography (pub/sub)
126
+ - context.tracker: Observability & state machines
127
+ """
128
+
129
+ from soorma import Worker, PlatformContext
130
+ from soorma.agents.worker import TaskContext
131
+
132
+
133
+ # Create a Worker agent
134
+ worker = Worker(
135
+ name="{name}",
136
+ description="{description}",
137
+ version="0.1.0",
138
+ capabilities=["example_task"], # Add your capabilities here
139
+ )
140
+
141
+
142
+ @worker.on_startup
143
+ async def startup():
144
+ """Called when the worker connects to the platform."""
145
+ print(f"🔧 {{worker.name}} is starting up...")
146
+ print(f" Registered capabilities: {{worker.capabilities}}")
147
+
148
+
149
+ @worker.on_shutdown
150
+ async def shutdown():
151
+ """Called when the worker is shutting down."""
152
+ print(f"👋 {{worker.name}} is shutting down...")
153
+
154
+
155
+ @worker.on_task("example_task")
156
+ async def handle_example_task(task: TaskContext, context: PlatformContext):
157
+ """
158
+ Handle incoming example_task assignments.
159
+
160
+ Replace this with your own task handlers. Each task handler:
161
+ 1. Receives a TaskContext with input data
162
+ 2. Has access to PlatformContext for platform services
163
+ 3. Returns a result dictionary
164
+
165
+ The platform automatically:
166
+ - Tracks task progress
167
+ - Publishes action-results on completion
168
+ - Handles errors and retries
169
+ """
170
+ print(f"Processing task: {{task.task_name}} ({{task.task_id}})")
171
+
172
+ # Report progress (optional)
173
+ await task.report_progress(0.5, "Processing...")
174
+
175
+ # Access shared memory (example)
176
+ # cached_data = await context.memory.retrieve(f"cache:{{task.data.get('key')}}")
177
+
178
+ # Your task logic here
179
+ result = {{
180
+ "message": "Hello from {name}!",
181
+ "processed": True,
182
+ }}
183
+
184
+ # Store results for other workers (optional)
185
+ # await context.memory.store(f"result:{{task.task_id}}", result)
186
+
187
+ return result
188
+
189
+
190
+ if __name__ == "__main__":
191
+ worker.run()
192
+ '''
193
+
194
+ # =============================================================================
195
+ # TOOL TEMPLATE
196
+ # =============================================================================
197
+ TOOL_PY_TEMPLATE = '''"""
198
+ {name} - A Soorma Tool Service.
199
+
200
+ Tools are the "utilities" of the DisCo architecture. They:
201
+ 1. Expose atomic, stateless operations
202
+ 2. Handle deterministic computations or API calls
203
+ 3. Are highly reusable across different workflows
204
+
205
+ Key differences from Workers:
206
+ - Tools are stateless (no memory between calls)
207
+ - Tools are deterministic (same input = same output)
208
+ - Tools are typically rules-based, not cognitive
209
+
210
+ The PlatformContext provides access to:
211
+ - context.registry: Service discovery
212
+ - context.bus: Event choreography
213
+ """
214
+
215
+ from soorma import Tool, PlatformContext
216
+ from soorma.agents.tool import ToolRequest
217
+
218
+
219
+ # Create a Tool service
220
+ tool = Tool(
221
+ name="{name}",
222
+ description="{description}",
223
+ version="0.1.0",
224
+ capabilities=["example_operation"], # Add your operations here
225
+ )
226
+
227
+
228
+ @tool.on_startup
229
+ async def startup():
230
+ """Called when the tool connects to the platform."""
231
+ print(f"🔨 {{tool.name}} is starting up...")
232
+ print(f" Available operations: {{tool.capabilities}}")
233
+
234
+
235
+ @tool.on_shutdown
236
+ async def shutdown():
237
+ """Called when the tool is shutting down."""
238
+ print(f"👋 {{tool.name}} is shutting down...")
239
+
240
+
241
+ @tool.on_invoke("example_operation")
242
+ async def handle_example_operation(request: ToolRequest, context: PlatformContext):
243
+ """
244
+ Handle incoming example_operation requests.
245
+
246
+ Replace this with your own operation handlers. Each handler:
247
+ 1. Receives a ToolRequest with input parameters
248
+ 2. Performs a stateless, deterministic operation
249
+ 3. Returns a result dictionary
250
+
251
+ Tools are ideal for:
252
+ - API integrations (weather, maps, search)
253
+ - Calculations and conversions
254
+ - Data transformations
255
+ - File parsing
256
+ """
257
+ print(f"Executing operation: {{request.operation}} ({{request.request_id}})")
258
+
259
+ # Your operation logic here
260
+ input_value = request.data.get("input", "default")
261
+
262
+ result = {{
263
+ "output": f"Processed: {{input_value}}",
264
+ "success": True,
265
+ }}
266
+
267
+ return result
268
+
269
+
270
+ if __name__ == "__main__":
271
+ tool.run()
272
+ '''
273
+
274
+ # Keep AGENT_PY_TEMPLATE as alias for backward compatibility
275
+ AGENT_PY_TEMPLATE = WORKER_PY_TEMPLATE
276
+
277
+ PYPROJECT_TEMPLATE = '''[project]
278
+ name = "{name}"
279
+ version = "0.1.0"
280
+ description = "{description}"
281
+ requires-python = ">=3.9"
282
+ dependencies = [
283
+ "soorma-core>=0.1.0",
284
+ ]
285
+
286
+ [project.optional-dependencies]
287
+ dev = [
288
+ "pytest>=8.0.0",
289
+ "pytest-asyncio>=0.23.0",
290
+ ]
291
+
292
+ [build-system]
293
+ requires = ["hatchling"]
294
+ build-backend = "hatchling.build"
295
+
296
+ [tool.hatch.build.targets.wheel]
297
+ packages = ["{package_name}"]
298
+ '''
299
+
300
+ README_TEMPLATE = '''# {name}
301
+
302
+ {description}
303
+
304
+ ## Getting Started
305
+
306
+ ### Prerequisites
307
+
308
+ - Python 3.9+
309
+ - Docker (for local development)
310
+
311
+ ### Installation
312
+
313
+ ```bash
314
+ # Create virtual environment
315
+ python -m venv .venv
316
+ source .venv/bin/activate # On Windows: .venv\\Scripts\\activate
317
+
318
+ # Install dependencies
319
+ pip install -e ".[dev]"
320
+ ```
321
+
322
+ ### Local Development
323
+
324
+ Start the local Soorma stack (Registry + NATS):
325
+
326
+ ```bash
327
+ soorma dev
328
+ ```
329
+
330
+ In another terminal, run your agent:
331
+
332
+ ```bash
333
+ python -m {package_name}.agent
334
+ ```
335
+
336
+ ### Deploy to Soorma Cloud
337
+
338
+ ```bash
339
+ soorma deploy
340
+ ```
341
+
342
+ ## Project Structure
343
+
344
+ ```
345
+ {name}/
346
+ ├── {package_name}/
347
+ │ ├── __init__.py
348
+ │ └── agent.py # Your agent code
349
+ ├── tests/
350
+ │ └── test_agent.py
351
+ ├── pyproject.toml
352
+ └── README.md
353
+ ```
354
+
355
+ ## Learn More
356
+
357
+ - [Soorma Documentation](https://soorma.ai/docs)
358
+ - [DisCo Architecture](https://soorma.ai/docs/disco)
359
+ '''
360
+
361
+ GITIGNORE_TEMPLATE = '''# Python
362
+ __pycache__/
363
+ *.py[cod]
364
+ *$py.class
365
+ *.so
366
+ .Python
367
+ build/
368
+ develop-eggs/
369
+ dist/
370
+ downloads/
371
+ eggs/
372
+ .eggs/
373
+ lib/
374
+ lib64/
375
+ parts/
376
+ sdist/
377
+ var/
378
+ wheels/
379
+ *.egg-info/
380
+ .installed.cfg
381
+ *.egg
382
+
383
+ # Virtual environments
384
+ .venv/
385
+ venv/
386
+ ENV/
387
+
388
+ # IDE
389
+ .idea/
390
+ .vscode/
391
+ *.swp
392
+ *.swo
393
+
394
+ # Testing
395
+ .pytest_cache/
396
+ .coverage
397
+ htmlcov/
398
+
399
+ # Soorma
400
+ .soorma/
401
+ '''
402
+
403
+ # =============================================================================
404
+ # TEST TEMPLATES
405
+ # =============================================================================
406
+ PLANNER_TEST_TEMPLATE = '''"""
407
+ Tests for {name} planner.
408
+ """
409
+
410
+ import pytest
411
+ from unittest.mock import AsyncMock, MagicMock
412
+
413
+
414
+ def test_planner_exists():
415
+ """Basic test to verify planner module loads."""
416
+ from {package_name} import agent
417
+ assert agent.planner.name == "{name}"
418
+ assert agent.planner.config.agent_type == "planner"
419
+
420
+
421
+ @pytest.mark.asyncio
422
+ async def test_startup():
423
+ """Test planner startup handler."""
424
+ from {package_name}.agent import startup
425
+ await startup()
426
+
427
+
428
+ @pytest.mark.asyncio
429
+ async def test_plan_example_goal():
430
+ """Test the example goal handler."""
431
+ from {package_name}.agent import plan_example_goal
432
+ from soorma.agents.planner import Goal
433
+
434
+ # Create mock goal
435
+ goal = Goal(
436
+ goal_type="example.goal",
437
+ data={{"input": "test_value"}},
438
+ )
439
+
440
+ # Create mock platform context
441
+ context = MagicMock()
442
+ context.registry.find_all = AsyncMock(return_value=[])
443
+
444
+ # Execute handler
445
+ plan = await plan_example_goal(goal, context)
446
+
447
+ # Verify plan
448
+ assert plan.goal == goal
449
+ assert len(plan.tasks) == 2
450
+ assert plan.tasks[0].name == "step_1"
451
+ assert plan.tasks[1].depends_on == ["step_1"]
452
+ '''
453
+
454
+ WORKER_TEST_TEMPLATE = '''"""
455
+ Tests for {name} worker.
456
+ """
457
+
458
+ import pytest
459
+ from unittest.mock import AsyncMock, MagicMock
460
+
461
+
462
+ def test_worker_exists():
463
+ """Basic test to verify worker module loads."""
464
+ from {package_name} import agent
465
+ assert agent.worker.name == "{name}"
466
+ assert "example_task" in agent.worker.capabilities
467
+
468
+
469
+ @pytest.mark.asyncio
470
+ async def test_startup():
471
+ """Test worker startup handler."""
472
+ from {package_name}.agent import startup
473
+ await startup()
474
+
475
+
476
+ @pytest.mark.asyncio
477
+ async def test_example_task():
478
+ """Test the example_task handler."""
479
+ from {package_name}.agent import handle_example_task
480
+ from soorma.agents.worker import TaskContext
481
+
482
+ # Create mock task context
483
+ task = TaskContext(
484
+ task_id="test-123",
485
+ task_name="example_task",
486
+ plan_id="plan-1",
487
+ goal_id="goal-1",
488
+ data={{"key": "value"}},
489
+ )
490
+ task.report_progress = AsyncMock()
491
+
492
+ # Create mock platform context
493
+ context = MagicMock()
494
+ context.memory.retrieve = AsyncMock(return_value=None)
495
+ context.memory.store = AsyncMock()
496
+
497
+ # Execute handler
498
+ result = await handle_example_task(task, context)
499
+
500
+ # Verify result
501
+ assert result["processed"] is True
502
+ assert "message" in result
503
+ '''
504
+
505
+ TOOL_TEST_TEMPLATE = '''"""
506
+ Tests for {name} tool.
507
+ """
508
+
509
+ import pytest
510
+ from unittest.mock import AsyncMock, MagicMock
511
+
512
+
513
+ def test_tool_exists():
514
+ """Basic test to verify tool module loads."""
515
+ from {package_name} import agent
516
+ assert agent.tool.name == "{name}"
517
+ assert "example_operation" in agent.tool.capabilities
518
+
519
+
520
+ @pytest.mark.asyncio
521
+ async def test_startup():
522
+ """Test tool startup handler."""
523
+ from {package_name}.agent import startup
524
+ await startup()
525
+
526
+
527
+ @pytest.mark.asyncio
528
+ async def test_example_operation():
529
+ """Test the example_operation handler."""
530
+ from {package_name}.agent import handle_example_operation
531
+ from soorma.agents.tool import ToolRequest
532
+
533
+ # Create mock request
534
+ request = ToolRequest(
535
+ operation="example_operation",
536
+ data={{"input": "test_value"}},
537
+ )
538
+
539
+ # Create mock platform context
540
+ context = MagicMock()
541
+
542
+ # Execute handler
543
+ result = await handle_example_operation(request, context)
544
+
545
+ # Verify result
546
+ assert result["success"] is True
547
+ assert "output" in result
548
+ '''
549
+
550
+ # Keep TEST_TEMPLATE as alias for backward compatibility (worker)
551
+ TEST_TEMPLATE = WORKER_TEST_TEMPLATE
552
+
553
+ INIT_TEMPLATE = '''"""
554
+ {name} - A Soorma AI Agent.
555
+ """
556
+
557
+ __version__ = "0.1.0"
558
+ '''
559
+
560
+
561
+ # Map agent types to their templates
562
+ AGENT_TEMPLATES = {
563
+ AgentType.planner: PLANNER_PY_TEMPLATE,
564
+ AgentType.worker: WORKER_PY_TEMPLATE,
565
+ AgentType.tool: TOOL_PY_TEMPLATE,
566
+ }
567
+
568
+ TEST_TEMPLATES = {
569
+ AgentType.planner: PLANNER_TEST_TEMPLATE,
570
+ AgentType.worker: WORKER_TEST_TEMPLATE,
571
+ AgentType.tool: TOOL_TEST_TEMPLATE,
572
+ }
573
+
574
+ AGENT_TYPE_DESCRIPTIONS = {
575
+ AgentType.planner: "Planner (strategic reasoning, goal decomposition)",
576
+ AgentType.worker: "Worker (domain-specific task execution)",
577
+ AgentType.tool: "Tool (atomic, stateless operations)",
578
+ }
579
+
580
+ AGENT_TYPE_EMOJIS = {
581
+ AgentType.planner: "🧠",
582
+ AgentType.worker: "🔧",
583
+ AgentType.tool: "🔨",
584
+ }
585
+
586
+
587
+ def init_project(
588
+ name: str = typer.Argument(
589
+ ...,
590
+ help="Name of the agent project to create.",
591
+ ),
592
+ agent_type: AgentType = typer.Option(
593
+ AgentType.worker,
594
+ "--type", "-t",
595
+ help="Type of agent to create: planner, worker, or tool.",
596
+ case_sensitive=False,
597
+ ),
598
+ description: str = typer.Option(
599
+ None,
600
+ "--description", "-d",
601
+ help="Short description of your agent.",
602
+ ),
603
+ output_dir: Optional[Path] = typer.Option(
604
+ None,
605
+ "--output", "-o",
606
+ help="Output directory. Defaults to current directory.",
607
+ ),
608
+ ):
609
+ """
610
+ Scaffold a new Soorma agent project.
611
+
612
+ Creates a new directory with boilerplate code, configuration,
613
+ and folder structure for a Soorma AI agent.
614
+
615
+ Examples:
616
+
617
+ soorma init my-worker # Default: Worker agent
618
+
619
+ soorma init my-planner --type planner # Planner agent
620
+
621
+ soorma init my-tool --type tool # Tool service
622
+ """
623
+ # Set default description based on type if not provided
624
+ if description is None:
625
+ description = f"A Soorma {agent_type.value.title()}"
626
+
627
+ # Determine output path
628
+ base_dir = output_dir or Path.cwd()
629
+ project_dir = base_dir / name
630
+
631
+ # Convert name to valid Python package name
632
+ package_name = name.lower().replace("-", "_").replace(" ", "_")
633
+
634
+ # Check if directory already exists
635
+ if project_dir.exists():
636
+ typer.echo(f"❌ Error: Directory '{name}' already exists.", err=True)
637
+ raise typer.Exit(1)
638
+
639
+ emoji = AGENT_TYPE_EMOJIS[agent_type]
640
+ typer.echo(f"{emoji} Creating new Soorma {agent_type.value} project: {name}")
641
+ typer.echo(f" Type: {AGENT_TYPE_DESCRIPTIONS[agent_type]}")
642
+ typer.echo("")
643
+
644
+ # Create directory structure
645
+ (project_dir / package_name).mkdir(parents=True)
646
+ (project_dir / "tests").mkdir()
647
+
648
+ # Create files
649
+ files_created = []
650
+
651
+ # pyproject.toml
652
+ (project_dir / "pyproject.toml").write_text(
653
+ PYPROJECT_TEMPLATE.format(
654
+ name=name,
655
+ description=description,
656
+ package_name=package_name,
657
+ )
658
+ )
659
+ files_created.append("pyproject.toml")
660
+
661
+ # README.md
662
+ (project_dir / "README.md").write_text(
663
+ README_TEMPLATE.format(
664
+ name=name,
665
+ description=description,
666
+ package_name=package_name,
667
+ )
668
+ )
669
+ files_created.append("README.md")
670
+
671
+ # .gitignore
672
+ (project_dir / ".gitignore").write_text(GITIGNORE_TEMPLATE)
673
+ files_created.append(".gitignore")
674
+
675
+ # Package __init__.py
676
+ (project_dir / package_name / "__init__.py").write_text(
677
+ INIT_TEMPLATE.format(name=name)
678
+ )
679
+ files_created.append(f"{package_name}/__init__.py")
680
+
681
+ # agent.py - use type-specific template
682
+ agent_template = AGENT_TEMPLATES[agent_type]
683
+ (project_dir / package_name / "agent.py").write_text(
684
+ agent_template.format(
685
+ name=name,
686
+ description=description,
687
+ )
688
+ )
689
+ files_created.append(f"{package_name}/agent.py")
690
+
691
+ # tests/__init__.py
692
+ (project_dir / "tests" / "__init__.py").write_text("")
693
+
694
+ # tests/test_agent.py - use type-specific template
695
+ test_template = TEST_TEMPLATES[agent_type]
696
+ (project_dir / "tests" / "test_agent.py").write_text(
697
+ test_template.format(
698
+ name=name,
699
+ package_name=package_name,
700
+ )
701
+ )
702
+ files_created.append("tests/test_agent.py")
703
+
704
+ # Success output
705
+ typer.echo("📁 Created project structure:")
706
+ for f in files_created:
707
+ typer.echo(f" ✓ {f}")
708
+ typer.echo("")
709
+ typer.echo("🎉 Success! Your agent project is ready.")
710
+ typer.echo("")
711
+ typer.echo("Next steps:")
712
+ typer.echo(f" cd {name}")
713
+ typer.echo(" python -m venv .venv")
714
+ typer.echo(" source .venv/bin/activate")
715
+ typer.echo(" pip install -e '.[dev]'")
716
+ typer.echo(" soorma dev")
717
+ typer.echo("")