jarviscore-framework 0.1.0__py3-none-any.whl → 0.1.1__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.
jarviscore/__init__.py CHANGED
@@ -4,11 +4,12 @@ JarvisCore - P2P Distributed Agent Framework
4
4
  A production-grade framework for building autonomous agent systems with:
5
5
  - Event-sourced state management (crash recovery, HITL support)
6
6
  - P2P coordination via SWIM protocol
7
- - Two execution profiles:
7
+ - Three execution profiles:
8
8
  * AutoAgent: LLM code generation (3 lines of user code)
9
9
  * CustomAgent: Framework-agnostic (LangChain, MCP, raw Python)
10
+ * @jarvis_agent: Decorator to wrap existing agents (1 line)
10
11
 
11
- Quick Start:
12
+ Quick Start (AutoAgent):
12
13
  from jarviscore import Mesh, AutoAgent
13
14
 
14
15
  class ScraperAgent(AutoAgent):
@@ -20,12 +21,33 @@ Quick Start:
20
21
  mesh.add(ScraperAgent)
21
22
  await mesh.start()
22
23
 
23
- results = await mesh.workflow("my-workflow", [
24
- {"agent": "scraper", "task": "Scrape example.com"}
24
+ Quick Start (Custom Profile with decorator):
25
+ from jarviscore import Mesh, jarvis_agent, JarvisContext
26
+
27
+ @jarvis_agent(role="processor", capabilities=["processing"])
28
+ class DataProcessor:
29
+ def run(self, data):
30
+ return {"processed": data * 2}
31
+
32
+ # With context access
33
+ @jarvis_agent(role="aggregator", capabilities=["aggregation"])
34
+ class Aggregator:
35
+ def run(self, task, ctx: JarvisContext):
36
+ prev = ctx.previous("step1")
37
+ return {"result": prev}
38
+
39
+ mesh = Mesh(mode="autonomous")
40
+ mesh.add(DataProcessor)
41
+ mesh.add(Aggregator)
42
+ await mesh.start()
43
+
44
+ results = await mesh.workflow("pipeline", [
45
+ {"agent": "processor", "task": "Process", "params": {"data": [1,2,3]}},
46
+ {"agent": "aggregator", "task": "Aggregate", "depends_on": [0]}
25
47
  ])
26
48
  """
27
49
 
28
- __version__ = "0.1.0"
50
+ __version__ = "0.1.1"
29
51
  __author__ = "JarvisCore Contributors"
30
52
  __license__ = "MIT"
31
53
 
@@ -38,6 +60,10 @@ from jarviscore.core.mesh import Mesh, MeshMode
38
60
  from jarviscore.profiles.autoagent import AutoAgent
39
61
  from jarviscore.profiles.customagent import CustomAgent
40
62
 
63
+ # Custom Profile: Decorator and Context
64
+ from jarviscore.adapter import jarvis_agent
65
+ from jarviscore.context import JarvisContext, MemoryAccessor, DependencyAccessor
66
+
41
67
  __all__ = [
42
68
  # Version
43
69
  "__version__",
@@ -51,4 +77,10 @@ __all__ = [
51
77
  # Profiles
52
78
  "AutoAgent",
53
79
  "CustomAgent",
80
+
81
+ # Custom Profile (decorator approach)
82
+ "jarvis_agent",
83
+ "JarvisContext",
84
+ "MemoryAccessor",
85
+ "DependencyAccessor",
54
86
  ]
@@ -0,0 +1,34 @@
1
+ """
2
+ Adapter module for JarvisCore Custom Profile.
3
+
4
+ Provides utilities to wrap existing agents for use with JarvisCore:
5
+ - @jarvis_agent: Decorator to convert any class into a JarvisCore agent
6
+
7
+ Example:
8
+ from jarviscore import jarvis_agent, Mesh, JarvisContext
9
+
10
+ @jarvis_agent(role="processor", capabilities=["processing"])
11
+ class DataProcessor:
12
+ def run(self, data):
13
+ return {"processed": data * 2}
14
+
15
+ # With context access
16
+ @jarvis_agent(role="aggregator", capabilities=["aggregation"])
17
+ class Aggregator:
18
+ def run(self, task, ctx: JarvisContext):
19
+ prev = ctx.previous("step1")
20
+ return {"aggregated": prev}
21
+
22
+ mesh = Mesh(mode="autonomous")
23
+ mesh.add(DataProcessor)
24
+ mesh.add(Aggregator)
25
+ await mesh.start()
26
+ """
27
+
28
+ from .decorator import jarvis_agent, detect_execute_method, EXECUTE_METHODS
29
+
30
+ __all__ = [
31
+ 'jarvis_agent',
32
+ 'detect_execute_method',
33
+ 'EXECUTE_METHODS',
34
+ ]
@@ -0,0 +1,332 @@
1
+ """
2
+ @jarvis_agent decorator - Convert any class into a JarvisCore agent.
3
+
4
+ Wraps existing classes (LangChain, CrewAI, raw Python) to work with
5
+ JarvisCore orchestration without requiring them to inherit from CustomAgent.
6
+ """
7
+ from typing import List, Optional, Type, Any
8
+ import inspect
9
+ import logging
10
+
11
+ from jarviscore.profiles.customagent import CustomAgent
12
+ from jarviscore.context import JarvisContext, create_context
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # Common method names to auto-detect
18
+ EXECUTE_METHODS = [
19
+ "run", # Most common
20
+ "invoke", # LangChain
21
+ "execute", # Generic
22
+ "call", # Callable pattern
23
+ "__call__", # Callable objects
24
+ "process", # Processing agents
25
+ "handle", # Handler pattern
26
+ ]
27
+
28
+
29
+ def detect_execute_method(cls: Type) -> Optional[str]:
30
+ """
31
+ Auto-detect the execute method on a class.
32
+
33
+ Checks for common method names in order of preference.
34
+ Only detects methods defined on the class itself, not inherited
35
+ from object (like __call__ from type).
36
+
37
+ Args:
38
+ cls: Class to inspect
39
+
40
+ Returns:
41
+ Method name or None if not found
42
+ """
43
+ for method_name in EXECUTE_METHODS:
44
+ # Check if method is defined on the class itself (not inherited from object)
45
+ # We walk the MRO but stop before 'object'
46
+ for klass in cls.__mro__:
47
+ if klass is object:
48
+ break
49
+ if method_name in klass.__dict__:
50
+ attr = getattr(cls, method_name)
51
+ if callable(attr):
52
+ return method_name
53
+ break
54
+ return None
55
+
56
+
57
+ def jarvis_agent(
58
+ role: str,
59
+ capabilities: List[str],
60
+ execute_method: Optional[str] = None
61
+ ):
62
+ """
63
+ Decorator to convert any class into a JarvisCore agent.
64
+
65
+ The decorated class can use any framework (LangChain, CrewAI, raw Python).
66
+ JarvisCore provides orchestration (data handoff, dependencies, memory).
67
+
68
+ Args:
69
+ role: Agent role identifier (used for step routing)
70
+ capabilities: List of capabilities (used for step matching)
71
+ execute_method: Method name to call for execution (auto-detected if not provided)
72
+
73
+ Returns:
74
+ Wrapped class that extends CustomAgent
75
+
76
+ Example:
77
+ # Simple agent
78
+ @jarvis_agent(role="processor", capabilities=["data_processing"])
79
+ class DataProcessor:
80
+ def run(self, data):
81
+ return {"processed": data * 2}
82
+
83
+ # Agent with context access
84
+ @jarvis_agent(role="aggregator", capabilities=["aggregation"])
85
+ class Aggregator:
86
+ def run(self, task, ctx: JarvisContext):
87
+ prev = ctx.previous("step1")
88
+ return {"result": prev}
89
+
90
+ # Agent with custom method name
91
+ @jarvis_agent(role="researcher", capabilities=["research"], execute_method="invoke")
92
+ class Researcher:
93
+ def invoke(self, query):
94
+ return {"findings": search(query)}
95
+
96
+ Usage:
97
+ mesh = Mesh(mode="autonomous")
98
+ mesh.add(DataProcessor)
99
+ await mesh.start()
100
+ results = await mesh.workflow("pipeline", [...])
101
+ """
102
+ def decorator(cls: Type) -> Type:
103
+ # Detect execute method if not provided
104
+ method_name = execute_method or detect_execute_method(cls)
105
+ if not method_name:
106
+ raise ValueError(
107
+ f"Could not detect execute method on {cls.__name__}. "
108
+ f"Please specify execute_method parameter or add one of: {EXECUTE_METHODS}"
109
+ )
110
+
111
+ # Verify method exists
112
+ if not hasattr(cls, method_name):
113
+ raise ValueError(
114
+ f"{cls.__name__} has no method '{method_name}'"
115
+ )
116
+
117
+ # Check if method expects context parameter
118
+ method = getattr(cls, method_name)
119
+ sig = inspect.signature(method)
120
+ params = list(sig.parameters.keys())
121
+
122
+ # Remove 'self' from params
123
+ if params and params[0] == 'self':
124
+ params = params[1:]
125
+
126
+ expects_context = 'ctx' in params or 'context' in params
127
+
128
+ logger.debug(
129
+ f"Wrapping {cls.__name__}: method={method_name}, "
130
+ f"expects_context={expects_context}, params={params}"
131
+ )
132
+
133
+ # Create wrapped CustomAgent subclass
134
+ class WrappedAgent(CustomAgent):
135
+ """Wrapped agent created by @jarvis_agent decorator."""
136
+
137
+ # Override class attributes from decorator
138
+ # (Can't use nonlocal role/capabilities directly due to scoping)
139
+ pass
140
+
141
+ # Set class attributes (must do this after class creation)
142
+ WrappedAgent.role = role
143
+ WrappedAgent.capabilities = capabilities
144
+
145
+ # Store metadata for introspection
146
+ WrappedAgent._wrapped_class = cls
147
+ WrappedAgent._execute_method = method_name
148
+ WrappedAgent._expects_context = expects_context
149
+ WrappedAgent._original_params = params
150
+
151
+ # Override __init__
152
+ original_init = WrappedAgent.__init__
153
+
154
+ def new_init(self, agent_id=None, **kwargs):
155
+ # Call CustomAgent init
156
+ CustomAgent.__init__(self, agent_id)
157
+
158
+ # Instantiate the wrapped class
159
+ try:
160
+ self._instance = cls(**kwargs) if kwargs else cls()
161
+ except TypeError:
162
+ # Class might not accept kwargs
163
+ self._instance = cls()
164
+
165
+ self._logger.debug(f"Created wrapped instance of {cls.__name__}")
166
+
167
+ WrappedAgent.__init__ = new_init
168
+
169
+ # Override setup
170
+ async def new_setup(self):
171
+ await CustomAgent.setup(self)
172
+
173
+ # Call wrapped class setup if it exists
174
+ if hasattr(self._instance, 'setup'):
175
+ setup_fn = self._instance.setup
176
+ if inspect.iscoroutinefunction(setup_fn):
177
+ await setup_fn()
178
+ else:
179
+ setup_fn()
180
+
181
+ self._logger.debug(f"Setup complete for wrapped {cls.__name__}")
182
+
183
+ WrappedAgent.setup = new_setup
184
+
185
+ # Override execute_task
186
+ async def new_execute_task(self, task: dict) -> dict:
187
+ """Execute by calling the wrapped class's method."""
188
+ # Get the execute method from instance
189
+ method = getattr(self._instance, method_name)
190
+
191
+ # Build context if method expects it
192
+ ctx = None
193
+ if expects_context:
194
+ # Get memory from mesh/workflow engine
195
+ memory_dict = {}
196
+ dep_manager = None
197
+
198
+ if self._mesh:
199
+ engine = getattr(self._mesh, '_workflow_engine', None)
200
+ if engine:
201
+ memory_dict = engine.memory
202
+ dep_manager = getattr(engine, 'dependency_manager', None)
203
+
204
+ ctx = create_context(
205
+ workflow_id=task.get('context', {}).get('workflow_id', 'unknown'),
206
+ step_id=task.get('context', {}).get('step_id', task.get('id', 'unknown')),
207
+ task=task.get('task', ''),
208
+ params=task.get('params', {}),
209
+ memory_dict=memory_dict,
210
+ dependency_manager=dep_manager
211
+ )
212
+
213
+ # Prepare arguments based on method signature
214
+ args = self._prepare_args(task, ctx, params)
215
+
216
+ # Call the method
217
+ try:
218
+ result = method(*args)
219
+
220
+ # Handle async methods
221
+ if inspect.isawaitable(result):
222
+ result = await result
223
+
224
+ except Exception as e:
225
+ self._logger.error(f"Error in {cls.__name__}.{method_name}: {e}")
226
+ return {
227
+ 'status': 'failure',
228
+ 'error': str(e),
229
+ 'agent': self.agent_id
230
+ }
231
+
232
+ # Normalize result to expected format
233
+ return self._normalize_result(result)
234
+
235
+ WrappedAgent.execute_task = new_execute_task
236
+
237
+ # Helper to prepare arguments
238
+ def prepare_args(self, task: dict, ctx: Optional[JarvisContext], param_names: List[str]) -> tuple:
239
+ """Prepare arguments for the wrapped method."""
240
+ args = []
241
+
242
+ # Get params, handling both dict and non-dict cases
243
+ task_params = task.get('params', {})
244
+ params_is_dict = isinstance(task_params, dict)
245
+
246
+ for param in param_names:
247
+ if param in ('ctx', 'context'):
248
+ args.append(ctx)
249
+ elif param == 'task':
250
+ args.append(task.get('task', task))
251
+ elif param == 'data':
252
+ # Try to get data from params or context
253
+ if params_is_dict:
254
+ args.append(task_params.get('data', task_params))
255
+ else:
256
+ args.append(task_params)
257
+ elif param == 'params':
258
+ args.append(task_params)
259
+ elif param in ('input', 'query', 'text', 'message'):
260
+ # Common input parameter names
261
+ if params_is_dict:
262
+ args.append(task.get('task', task_params.get(param, '')))
263
+ else:
264
+ args.append(task.get('task', task_params))
265
+ else:
266
+ # Try to get from params, fall back to task dict
267
+ if params_is_dict and param in task_params:
268
+ args.append(task_params[param])
269
+ elif param in task:
270
+ args.append(task[param])
271
+ else:
272
+ # Pass whole params as fallback
273
+ args.append(task_params)
274
+ break # Only do this once
275
+
276
+ # If no args were determined, pass the task/params
277
+ if not args:
278
+ if 'params' in task:
279
+ args.append(task_params)
280
+ else:
281
+ args.append(task)
282
+
283
+ return tuple(args)
284
+
285
+ WrappedAgent._prepare_args = prepare_args
286
+
287
+ # Helper to normalize result
288
+ def normalize_result(self, result: Any) -> dict:
289
+ """Normalize result to expected format."""
290
+ if isinstance(result, dict):
291
+ if 'status' not in result:
292
+ return {
293
+ 'status': 'success',
294
+ 'output': result,
295
+ 'agent': self.agent_id
296
+ }
297
+ if 'agent' not in result:
298
+ result['agent'] = self.agent_id
299
+ return result
300
+ else:
301
+ return {
302
+ 'status': 'success',
303
+ 'output': result,
304
+ 'agent': self.agent_id
305
+ }
306
+
307
+ WrappedAgent._normalize_result = normalize_result
308
+
309
+ # Override teardown
310
+ async def new_teardown(self):
311
+ # Call wrapped class teardown if it exists
312
+ if hasattr(self._instance, 'teardown'):
313
+ teardown_fn = self._instance.teardown
314
+ if inspect.iscoroutinefunction(teardown_fn):
315
+ await teardown_fn()
316
+ else:
317
+ teardown_fn()
318
+
319
+ await CustomAgent.teardown(self)
320
+ self._logger.debug(f"Teardown complete for wrapped {cls.__name__}")
321
+
322
+ WrappedAgent.teardown = new_teardown
323
+
324
+ # Copy class metadata
325
+ WrappedAgent.__name__ = cls.__name__
326
+ WrappedAgent.__qualname__ = cls.__qualname__
327
+ WrappedAgent.__doc__ = cls.__doc__ or f"Wrapped agent from {cls.__name__}"
328
+ WrappedAgent.__module__ = cls.__module__
329
+
330
+ return WrappedAgent
331
+
332
+ return decorator
jarviscore/cli/check.py CHANGED
@@ -222,14 +222,17 @@ class HealthChecker:
222
222
  return False
223
223
 
224
224
  async def _test_gemini(self) -> bool:
225
- """Test Gemini connectivity."""
225
+ """Test Gemini connectivity using the new google.genai SDK."""
226
226
  try:
227
- import google.generativeai as genai
227
+ from google import genai
228
228
 
229
- genai.configure(api_key=os.getenv('GEMINI_API_KEY'))
230
- model = genai.GenerativeModel(os.getenv('GEMINI_MODEL', 'gemini-1.5-flash'))
229
+ client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
230
+ model_name = os.getenv('GEMINI_MODEL', 'gemini-2.0-flash')
231
231
 
232
- response = await model.generate_content_async("Reply with just 'OK'")
232
+ response = await client.aio.models.generate_content(
233
+ model=model_name,
234
+ contents="Reply with just 'OK'"
235
+ )
233
236
  return 'OK' in response.text.upper()
234
237
 
235
238
  except Exception:
@@ -291,13 +294,15 @@ class HealthChecker:
291
294
  print("\n" + "="*70)
292
295
  print(" Next Steps")
293
296
  print("="*70)
294
- print("\n1. Copy .env.example to .env:")
297
+ print("\n1. Initialize project (creates .env.example and examples):")
298
+ print(" python -m jarviscore.cli.scaffold --examples")
299
+ print("\n2. Configure your environment:")
295
300
  print(" cp .env.example .env")
296
- print("\n2. Add your LLM API key to .env (choose one):")
297
- print(" - CLAUDE_API_KEY=sk-ant-...")
298
- print(" - AZURE_API_KEY=...")
299
- print(" - GEMINI_API_KEY=...")
300
- print(" - LLM_ENDPOINT=http://localhost:8000 (for local vLLM)")
301
+ print(" # Edit .env and add one of:")
302
+ print(" # CLAUDE_API_KEY=sk-ant-...")
303
+ print(" # AZURE_API_KEY=...")
304
+ print(" # GEMINI_API_KEY=...")
305
+ print(" # LLM_ENDPOINT=http://localhost:8000 (for local vLLM)")
301
306
  print("\n3. Run health check again:")
302
307
  print(" python -m jarviscore.cli.check --validate-llm")
303
308
  print("\n4. Try the smoke test:")
@@ -309,8 +314,8 @@ class HealthChecker:
309
314
  print("\n✓ All checks passed! Ready to use JarvisCore.\n")
310
315
  print("Next steps:")
311
316
  print(" 1. Run smoke test: python -m jarviscore.cli.smoketest")
312
- print(" 2. Try examples: python examples/calculator_agent_example.py")
313
- print(" 3. Read guide: docs/GETTING_STARTED.md")
317
+ print(" 2. Get examples: python -m jarviscore.cli.scaffold --examples")
318
+ print(" 3. Try example: python examples/calculator_agent_example.py")
314
319
  print()
315
320
 
316
321
  return True
@@ -0,0 +1,178 @@
1
+ """
2
+ JarvisCore Project Initialization CLI
3
+
4
+ Scaffolds a new JarvisCore project with configuration and examples.
5
+
6
+ Usage:
7
+ python -m jarviscore.cli.scaffold # Create .env from template
8
+ python -m jarviscore.cli.scaffold --examples # Also copy example files
9
+ python -m jarviscore.cli.scaffold --force # Overwrite existing files
10
+ """
11
+
12
+ import sys
13
+ import shutil
14
+ from pathlib import Path
15
+ from importlib import resources
16
+ import argparse
17
+
18
+
19
+ def get_data_path() -> Path:
20
+ """Get path to the data directory within the package."""
21
+ # Python 3.9+ approach using importlib.resources
22
+ try:
23
+ # resources.files() returns a Traversable, convert to Path
24
+ data_path = resources.files('jarviscore.data')
25
+ return Path(str(data_path))
26
+ except (TypeError, AttributeError):
27
+ # Fallback for older Python or if resources.files doesn't work
28
+ import jarviscore.data
29
+ return Path(jarviscore.data.__file__).parent
30
+
31
+
32
+ def copy_env_example(dest_dir: Path, force: bool = False) -> bool:
33
+ """
34
+ Copy .env.example to destination directory.
35
+
36
+ Args:
37
+ dest_dir: Destination directory
38
+ force: Overwrite if exists
39
+
40
+ Returns:
41
+ True if copied, False if skipped
42
+ """
43
+ data_path = get_data_path()
44
+ src = data_path / '.env.example'
45
+ dest = dest_dir / '.env.example'
46
+
47
+ if not src.exists():
48
+ print(f"✗ Source file not found: {src}")
49
+ return False
50
+
51
+ if dest.exists() and not force:
52
+ print(f"⚠ {dest.name} already exists (use --force to overwrite)")
53
+ return False
54
+
55
+ shutil.copy2(src, dest)
56
+ print(f"✓ Created {dest.name}")
57
+ return True
58
+
59
+
60
+ def copy_examples(dest_dir: Path, force: bool = False) -> bool:
61
+ """
62
+ Copy example files to destination directory.
63
+
64
+ Args:
65
+ dest_dir: Destination directory
66
+ force: Overwrite if exists
67
+
68
+ Returns:
69
+ True if copied, False if skipped
70
+ """
71
+ data_path = get_data_path()
72
+ src = data_path / 'examples'
73
+ dest = dest_dir / 'examples'
74
+
75
+ if not src.exists():
76
+ print(f"✗ Examples directory not found: {src}")
77
+ return False
78
+
79
+ if dest.exists() and not force:
80
+ print(f"⚠ examples/ directory already exists (use --force to overwrite)")
81
+ return False
82
+
83
+ if dest.exists() and force:
84
+ shutil.rmtree(dest)
85
+
86
+ shutil.copytree(src, dest)
87
+
88
+ # Count files copied
89
+ file_count = sum(1 for _ in dest.glob('*.py'))
90
+ print(f"✓ Created examples/ directory ({file_count} files)")
91
+ return True
92
+
93
+
94
+ def print_header():
95
+ """Print initialization header."""
96
+ print("\n" + "=" * 60)
97
+ print(" JarvisCore Project Initialization")
98
+ print("=" * 60 + "\n")
99
+
100
+
101
+ def print_next_steps(env_created: bool, examples_created: bool):
102
+ """Print next steps after initialization."""
103
+ print("\n" + "=" * 60)
104
+ print(" Next Steps")
105
+ print("=" * 60)
106
+
107
+ steps = []
108
+
109
+ if env_created:
110
+ steps.append("1. Copy and configure your environment:\n"
111
+ " cp .env.example .env\n"
112
+ " # Edit .env and add your LLM API key")
113
+
114
+ steps.append(f"{'2' if env_created else '1'}. Validate your setup:\n"
115
+ " python -m jarviscore.cli.check --validate-llm")
116
+
117
+ steps.append(f"{'3' if env_created else '2'}. Run smoke test:\n"
118
+ " python -m jarviscore.cli.smoketest")
119
+
120
+ if examples_created:
121
+ steps.append(f"{'4' if env_created else '3'}. Try an example:\n"
122
+ " python examples/calculator_agent_example.py")
123
+
124
+ for step in steps:
125
+ print(f"\n{step}")
126
+
127
+ print()
128
+
129
+
130
+ def main():
131
+ """CLI entry point."""
132
+ parser = argparse.ArgumentParser(
133
+ description='Initialize a new JarvisCore project'
134
+ )
135
+ parser.add_argument(
136
+ '--examples',
137
+ action='store_true',
138
+ help='Also copy example agent files'
139
+ )
140
+ parser.add_argument(
141
+ '--force',
142
+ action='store_true',
143
+ help='Overwrite existing files'
144
+ )
145
+ parser.add_argument(
146
+ '--dir',
147
+ type=str,
148
+ default='.',
149
+ help='Target directory (default: current directory)'
150
+ )
151
+
152
+ args = parser.parse_args()
153
+ dest_dir = Path(args.dir).resolve()
154
+
155
+ print_header()
156
+ print(f"Initializing in: {dest_dir}\n")
157
+
158
+ # Ensure destination exists
159
+ dest_dir.mkdir(parents=True, exist_ok=True)
160
+
161
+ # Copy files
162
+ env_created = copy_env_example(dest_dir, args.force)
163
+
164
+ examples_created = False
165
+ if args.examples:
166
+ examples_created = copy_examples(dest_dir, args.force)
167
+
168
+ # Summary
169
+ if env_created or examples_created:
170
+ print_next_steps(env_created, examples_created)
171
+ sys.exit(0)
172
+ else:
173
+ print("\n⚠ No files were created. Use --force to overwrite existing files.")
174
+ sys.exit(1)
175
+
176
+
177
+ if __name__ == '__main__':
178
+ main()