spawnio 1.0.5__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.
Files changed (56) hide show
  1. spawn/__init__.py +6 -0
  2. spawn/cli/__init__.py +0 -0
  3. spawn/cli/app.py +218 -0
  4. spawn/cli/noninteractive.py +204 -0
  5. spawn/cli/prompts.py +267 -0
  6. spawn/core/__init__.py +0 -0
  7. spawn/core/exceptions.py +7 -0
  8. spawn/core/models.py +21 -0
  9. spawn/core/registry.py +152 -0
  10. spawn/generators/__init__.py +0 -0
  11. spawn/generators/custom_structure.py +508 -0
  12. spawn/generators/project_generator.py +115 -0
  13. spawn/github/__init__.py +0 -0
  14. spawn/github/exceptions.py +5 -0
  15. spawn/github/publisher.py +69 -0
  16. spawn/github/validators.py +23 -0
  17. spawn/templates/__init__.py +0 -0
  18. spawn/templates/agent/__init__.py +182 -0
  19. spawn/templates/agent/content.py +374 -0
  20. spawn/templates/automation/__init__.py +100 -0
  21. spawn/templates/automation/content.py +181 -0
  22. spawn/templates/backend_api/__init__.py +243 -0
  23. spawn/templates/backend_api/content.py +509 -0
  24. spawn/templates/base.py +58 -0
  25. spawn/templates/chatbot/__init__.py +243 -0
  26. spawn/templates/chatbot/content.py +668 -0
  27. spawn/templates/cli_application/__init__.py +211 -0
  28. spawn/templates/cli_application/content.py +512 -0
  29. spawn/templates/data_project/__init__.py +125 -0
  30. spawn/templates/data_project/analysis/__init__.py +74 -0
  31. spawn/templates/data_project/analysis/content.py +190 -0
  32. spawn/templates/data_project/dashboard/__init__.py +61 -0
  33. spawn/templates/data_project/dashboard/content.py +135 -0
  34. spawn/templates/data_project/etl/__init__.py +66 -0
  35. spawn/templates/data_project/etl/content.py +239 -0
  36. spawn/templates/data_project/ml/__init__.py +62 -0
  37. spawn/templates/data_project/ml/content.py +239 -0
  38. spawn/templates/mcp_server/__init__.py +83 -0
  39. spawn/templates/mcp_server/content.py +161 -0
  40. spawn/templates/rag/__init__.py +124 -0
  41. spawn/templates/rag/content.py +463 -0
  42. spawn/templates/shared_content.py +89 -0
  43. spawn/utils/__init__.py +0 -0
  44. spawn/utils/banner.py +41 -0
  45. spawn/utils/console.py +3 -0
  46. spawn/utils/doctor.py +594 -0
  47. spawn/utils/git.py +132 -0
  48. spawn/utils/success.py +47 -0
  49. spawn/utils/theme.py +25 -0
  50. spawn/utils/uv.py +61 -0
  51. spawn/utils/validators.py +15 -0
  52. spawnio-1.0.5.dist-info/METADATA +798 -0
  53. spawnio-1.0.5.dist-info/RECORD +56 -0
  54. spawnio-1.0.5.dist-info/WHEEL +4 -0
  55. spawnio-1.0.5.dist-info/entry_points.txt +2 -0
  56. spawnio-1.0.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,668 @@
1
+ INIT_CONTENT = ""
2
+
3
+ # ─── Shared content ───────────────────────────────────────────────────────
4
+
5
+ CHAT_CONTENT = """\
6
+ from src.providers.llm import get_llm_response
7
+ from src.memory.history import append_user, append_assistant, get_history
8
+ from src.config.settings import get_system_prompt
9
+
10
+
11
+ def get_response(user_input: str) -> str:
12
+ append_user(user_input)
13
+ history = get_history()
14
+ system_prompt = get_system_prompt()
15
+ response = get_llm_response(history, system_prompt)
16
+ append_assistant(response)
17
+ return response
18
+ """
19
+
20
+ MEMORY_HISTORY_CONTENT_BASE = """\
21
+ _history: list[dict] = []
22
+
23
+
24
+ def append_user(content: str) -> None:
25
+ _history.append({{"role": "user", "content": content}})
26
+
27
+
28
+ def append_assistant(content: str) -> None:
29
+ _history.append({{"role": "assistant", "content": content}})
30
+
31
+
32
+ def get_history() -> list[dict]:
33
+ return list(_history)
34
+
35
+
36
+ def clear() -> None:
37
+ _history.clear()
38
+ """
39
+
40
+ MEMORY_HISTORY_CONTENT_PYDANTIC = """\
41
+ from __future__ import annotations
42
+
43
+ from typing import TYPE_CHECKING
44
+
45
+ if TYPE_CHECKING:
46
+ from pydantic_ai.messages import ModelMessage
47
+
48
+ _history: list[dict] = []
49
+ _pai_history: list[ModelMessage] = []
50
+
51
+
52
+ def append_user(content: str) -> None:
53
+ _history.append({{"role": "user", "content": content}})
54
+
55
+
56
+ def append_assistant(content: str) -> None:
57
+ _history.append({{"role": "assistant", "content": content}})
58
+
59
+
60
+ def get_history() -> list[dict]:
61
+ return list(_history)
62
+
63
+
64
+ def get_pai_history() -> list[ModelMessage]:
65
+ return list(_pai_history)
66
+
67
+
68
+ def append_pai_messages(messages: list[ModelMessage]) -> None:
69
+ _pai_history.extend(messages)
70
+
71
+
72
+ def clear() -> None:
73
+ _history.clear()
74
+ _pai_history.clear()
75
+ """
76
+
77
+ SYSTEM_PROMPT_TXT_CONTENT = """\
78
+ You are a helpful AI assistant.
79
+ Answer questions clearly and concisely.
80
+ """
81
+
82
+ SETTINGS_CONTENT = """\
83
+ from pathlib import Path
84
+
85
+ from dotenv import load_dotenv
86
+
87
+
88
+ def load_env() -> None:
89
+ load_dotenv()
90
+
91
+
92
+ def get_system_prompt() -> str:
93
+ prompt_path = Path(__file__).parent.parent / "prompts" / "system.txt"
94
+ return prompt_path.read_text(encoding="utf-8").strip()
95
+ """
96
+
97
+ # ─── PydanticAI ───────────────────────────────────────────────────────────
98
+
99
+ PYDANTIC_AI_OPENAI_LLM_CONTENT = """\
100
+ import os
101
+
102
+ from pydantic_ai import Agent
103
+
104
+ from src.memory.history import get_pai_history, append_pai_messages
105
+
106
+
107
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
108
+ model = os.getenv("MODEL", "openai:gpt-4o-mini")
109
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
110
+ prompt = user_messages[-1] if user_messages else ""
111
+ agent = Agent(model, system_prompt=system_prompt)
112
+ result = agent.run_sync(prompt, message_history=get_pai_history())
113
+ append_pai_messages(result.new_messages())
114
+ return result.output
115
+ """
116
+
117
+ PYDANTIC_AI_ANTHROPIC_LLM_CONTENT = """\
118
+ import os
119
+
120
+ from pydantic_ai import Agent
121
+
122
+ from src.memory.history import get_pai_history, append_pai_messages
123
+
124
+
125
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
126
+ model = os.getenv("MODEL", "anthropic:claude-3-5-haiku-latest")
127
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
128
+ prompt = user_messages[-1] if user_messages else ""
129
+ agent = Agent(model, system_prompt=system_prompt)
130
+ result = agent.run_sync(prompt, message_history=get_pai_history())
131
+ append_pai_messages(result.new_messages())
132
+ return result.output
133
+ """
134
+
135
+ PYDANTIC_AI_GEMINI_LLM_CONTENT = """\
136
+ import os
137
+
138
+ from pydantic_ai import Agent
139
+
140
+ from src.memory.history import get_pai_history, append_pai_messages
141
+
142
+
143
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
144
+ model = os.getenv("MODEL", "google:gemini-2.0-flash")
145
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
146
+ prompt = user_messages[-1] if user_messages else ""
147
+ agent = Agent(model, system_prompt=system_prompt)
148
+ result = agent.run_sync(prompt, message_history=get_pai_history())
149
+ append_pai_messages(result.new_messages())
150
+ return result.output
151
+ """
152
+
153
+ PYDANTIC_AI_OPENROUTER_LLM_CONTENT = """\
154
+ import os
155
+
156
+ from pydantic_ai import Agent
157
+ from pydantic_ai.models.openai import OpenAIChatModel
158
+ from pydantic_ai.providers.openai import OpenAIProvider
159
+
160
+ from src.memory.history import get_pai_history, append_pai_messages
161
+
162
+
163
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
164
+ model_name = os.getenv("MODEL", "openai/gpt-4o-mini")
165
+ api_key = os.getenv("OPENROUTER_API_KEY", "")
166
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
167
+ prompt = user_messages[-1] if user_messages else ""
168
+ provider = OpenAIProvider(
169
+ api_key=api_key,
170
+ base_url="https://openrouter.ai/api/v1",
171
+ )
172
+ model = OpenAIChatModel(model_name, provider=provider)
173
+ agent = Agent(model, system_prompt=system_prompt)
174
+ result = agent.run_sync(prompt, message_history=get_pai_history())
175
+ append_pai_messages(result.new_messages())
176
+ return result.output
177
+ """
178
+
179
+ PYDANTIC_AI_OLLAMA_LLM_CONTENT = """\
180
+ import os
181
+
182
+ from pydantic_ai import Agent
183
+ from pydantic_ai.models.openai import OpenAIChatModel
184
+ from pydantic_ai.providers.openai import OpenAIProvider
185
+
186
+ from src.memory.history import get_pai_history, append_pai_messages
187
+
188
+
189
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
190
+ model_name = os.getenv("MODEL", "llama3.2")
191
+ base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
192
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
193
+ prompt = user_messages[-1] if user_messages else ""
194
+ provider = OpenAIProvider(api_key="ollama", base_url=base_url)
195
+ model = OpenAIChatModel(model_name, provider=provider)
196
+ agent = Agent(model, system_prompt=system_prompt)
197
+ result = agent.run_sync(prompt, message_history=get_pai_history())
198
+ append_pai_messages(result.new_messages())
199
+ return result.output
200
+ """
201
+
202
+ PYDANTIC_AI_GROQ_LLM_CONTENT = """\
203
+ import os
204
+
205
+ from pydantic_ai import Agent
206
+
207
+ from src.memory.history import get_pai_history, append_pai_messages
208
+
209
+
210
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
211
+ model = os.getenv("MODEL", "groq:llama-3.1-8b-instant")
212
+ user_messages = [m["content"] for m in messages if m["role"] == "user"]
213
+ prompt = user_messages[-1] if user_messages else ""
214
+ agent = Agent(model, system_prompt=system_prompt)
215
+ result = agent.run_sync(prompt, message_history=get_pai_history())
216
+ append_pai_messages(result.new_messages())
217
+ return result.output
218
+ """
219
+
220
+ # ─── OpenAI SDK ───────────────────────────────────────────────────────────
221
+
222
+ OPENAI_SDK_OPENAI_LLM_CONTENT = """\
223
+ import os
224
+
225
+ from openai import OpenAI
226
+
227
+
228
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
229
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", ""))
230
+ model = os.getenv("MODEL", "gpt-4o-mini")
231
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
232
+ response = client.chat.completions.create(model=model, messages=full_messages)
233
+ return response.choices[0].message.content or ""
234
+ """
235
+
236
+ OPENAI_SDK_OPENROUTER_LLM_CONTENT = """\
237
+ import os
238
+
239
+ from openai import OpenAI
240
+
241
+
242
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
243
+ client = OpenAI(
244
+ api_key=os.getenv("OPENROUTER_API_KEY", ""),
245
+ base_url="https://openrouter.ai/api/v1",
246
+ )
247
+ model = os.getenv("MODEL", "openai/gpt-4o-mini")
248
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
249
+ response = client.chat.completions.create(model=model, messages=full_messages)
250
+ return response.choices[0].message.content or ""
251
+ """
252
+
253
+ OPENAI_SDK_GEMINI_LLM_CONTENT = """\
254
+ import os
255
+
256
+ from openai import OpenAI
257
+
258
+
259
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
260
+ client = OpenAI(
261
+ api_key=os.getenv("GOOGLE_API_KEY", ""),
262
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
263
+ )
264
+ model = os.getenv("MODEL", "gemini-1.5-flash")
265
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
266
+ response = client.chat.completions.create(model=model, messages=full_messages)
267
+ return response.choices[0].message.content or ""
268
+ """
269
+
270
+ OPENAI_SDK_GROQ_LLM_CONTENT = """\
271
+ import os
272
+
273
+ from openai import OpenAI
274
+
275
+
276
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
277
+ client = OpenAI(
278
+ api_key=os.getenv("GROQ_API_KEY", ""),
279
+ base_url="https://api.groq.com/openai/v1",
280
+ )
281
+ model = os.getenv("MODEL", "llama-3.1-8b-instant")
282
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
283
+ response = client.chat.completions.create(model=model, messages=full_messages)
284
+ return response.choices[0].message.content or ""
285
+ """
286
+
287
+ # ─── LiteLLM ──────────────────────────────────────────────────────────────
288
+
289
+ LITELLM_OPENAI_LLM_CONTENT = """\
290
+ import os
291
+
292
+ import litellm
293
+
294
+
295
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
296
+ model = os.getenv("MODEL", "gpt-4o-mini")
297
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
298
+ response = litellm.completion(model=model, messages=full_messages)
299
+ return response.choices[0].message.content or ""
300
+ """
301
+
302
+ LITELLM_ANTHROPIC_LLM_CONTENT = """\
303
+ import os
304
+
305
+ import litellm
306
+
307
+
308
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
309
+ model = os.getenv("MODEL", "claude-3-5-haiku-latest")
310
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
311
+ response = litellm.completion(model=model, messages=full_messages)
312
+ return response.choices[0].message.content or ""
313
+ """
314
+
315
+ LITELLM_GEMINI_LLM_CONTENT = """\
316
+ import os
317
+
318
+ import litellm
319
+
320
+
321
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
322
+ model = os.getenv("MODEL", "gemini/gemini-1.5-flash")
323
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
324
+ response = litellm.completion(model=model, messages=full_messages)
325
+ return response.choices[0].message.content or ""
326
+ """
327
+
328
+ LITELLM_OPENROUTER_LLM_CONTENT = """\
329
+ import os
330
+
331
+ import litellm
332
+
333
+
334
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
335
+ model = os.getenv("MODEL", "openrouter/openai/gpt-4o-mini")
336
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
337
+ response = litellm.completion(model=model, messages=full_messages)
338
+ return response.choices[0].message.content or ""
339
+ """
340
+
341
+ LITELLM_OLLAMA_LLM_CONTENT = """\
342
+ import os
343
+
344
+ import litellm
345
+
346
+
347
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
348
+ model = os.getenv("MODEL", "ollama/llama3.2")
349
+ base_url = os.getenv("OLLAMA_API_BASE", "http://localhost:11434")
350
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
351
+ response = litellm.completion(model=model, messages=full_messages, api_base=base_url)
352
+ return response.choices[0].message.content or ""
353
+ """
354
+
355
+ LITELLM_GROQ_LLM_CONTENT = """\
356
+ import os
357
+
358
+ import litellm
359
+
360
+
361
+ def get_llm_response(messages: list[dict], system_prompt: str) -> str:
362
+ model = os.getenv("MODEL", "groq/llama-3.1-8b-instant")
363
+ full_messages = [{{"role": "system", "content": system_prompt}}] + messages
364
+ response = litellm.completion(model=model, messages=full_messages)
365
+ return response.choices[0].message.content or ""
366
+ """
367
+
368
+ # ─── Main content ─────────────────────────────────────────────────────────
369
+
370
+ MAIN_CONTENT_NO_RICH = """\
371
+ from src.chatbot.chat import get_response
372
+ from src.config.settings import load_env
373
+
374
+
375
+ def main() -> None:
376
+ load_env()
377
+ print("Chatbot ready. Type 'quit' to exit.\\n")
378
+ while True:
379
+ user_input = input("You: ").strip()
380
+ if not user_input:
381
+ continue
382
+ if user_input.lower() in ("quit", "exit"):
383
+ break
384
+ response = get_response(user_input)
385
+ print(f"Bot: {{response}}\\n")
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()
390
+ """
391
+
392
+ MAIN_CONTENT_RICH = """\
393
+ from rich.console import Console
394
+ from rich.panel import Panel
395
+
396
+ from src.chatbot.chat import get_response
397
+ from src.config.settings import load_env
398
+
399
+ console = Console()
400
+
401
+
402
+ def main() -> None:
403
+ load_env()
404
+ console.print(Panel.fit("[bold cyan]Chatbot[/bold cyan] — type [dim]quit[/dim] to exit"))
405
+ while True:
406
+ user_input = console.input("[bold yellow]You:[/bold yellow] ").strip()
407
+ if not user_input:
408
+ continue
409
+ if user_input.lower() in ("quit", "exit"):
410
+ break
411
+ response = get_response(user_input)
412
+ console.print(f"[bold green]Bot:[/bold green] {{response}}\\n")
413
+
414
+
415
+ if __name__ == "__main__":
416
+ main()
417
+ """
418
+
419
+ # ─── Env examples (provider-specific) ─────────────────────────────────────
420
+
421
+ ENV_OPENAI = """\
422
+ APP_NAME={project_name}
423
+ OPENAI_API_KEY=
424
+ MODEL=gpt-4o-mini
425
+ """
426
+
427
+ ENV_ANTHROPIC = """\
428
+ APP_NAME={project_name}
429
+ ANTHROPIC_API_KEY=
430
+ MODEL=claude-3-5-haiku-latest
431
+ """
432
+
433
+ ENV_GEMINI = """\
434
+ APP_NAME={project_name}
435
+ GOOGLE_API_KEY=
436
+ MODEL=gemini-1.5-flash
437
+ """
438
+
439
+ ENV_OPENROUTER = """\
440
+ APP_NAME={project_name}
441
+ OPENROUTER_API_KEY=
442
+ MODEL=openai/gpt-4o-mini
443
+ """
444
+
445
+ ENV_OLLAMA = """\
446
+ APP_NAME={project_name}
447
+ OLLAMA_BASE_URL=http://localhost:11434
448
+ MODEL=llama3.2
449
+ """
450
+
451
+ # litellm+ollama uses a different env var for the base URL
452
+ ENV_LITELLM_OLLAMA = """\
453
+ APP_NAME={project_name}
454
+ OLLAMA_API_BASE=http://localhost:11434
455
+ MODEL=ollama/llama3.2
456
+ """
457
+
458
+ ENV_GROQ = """\
459
+ APP_NAME={project_name}
460
+ GROQ_API_KEY=
461
+ MODEL=llama-3.1-8b-instant
462
+ """
463
+
464
+ # Provider-prefixed MODEL for pydantic-ai variants
465
+
466
+ ENV_PYDANTIC_OPENAI = """\
467
+ APP_NAME={project_name}
468
+ OPENAI_API_KEY=
469
+ MODEL=openai:gpt-4o-mini
470
+ """
471
+
472
+ ENV_PYDANTIC_ANTHROPIC = """\
473
+ APP_NAME={project_name}
474
+ ANTHROPIC_API_KEY=
475
+ MODEL=anthropic:claude-3-5-haiku-latest
476
+ """
477
+
478
+ ENV_PYDANTIC_GEMINI = """\
479
+ APP_NAME={project_name}
480
+ GOOGLE_API_KEY=
481
+ MODEL=google:gemini-2.0-flash
482
+ """
483
+
484
+ ENV_PYDANTIC_OPENROUTER = """\
485
+ APP_NAME={project_name}
486
+ OPENROUTER_API_KEY=
487
+ MODEL=openai/gpt-4o-mini
488
+ """
489
+
490
+ ENV_PYDANTIC_OLLAMA = """\
491
+ APP_NAME={project_name}
492
+ OLLAMA_BASE_URL=http://localhost:11434
493
+ MODEL=llama3.2
494
+ """
495
+
496
+ ENV_PYDANTIC_GROQ = """\
497
+ APP_NAME={project_name}
498
+ GROQ_API_KEY=
499
+ MODEL=groq:llama-3.1-8b-instant
500
+ """
501
+
502
+ ENV_LITELLM_GROQ = """\
503
+ APP_NAME={project_name}
504
+ GROQ_API_KEY=
505
+ MODEL=groq/llama-3.1-8b-instant
506
+ """
507
+
508
+ # ─── Test content ─────────────────────────────────────────────────────────
509
+
510
+ # BUG FIX: removed unused imports MagicMock and get_llm_response
511
+ # which caused ruff F401 failures in generated projects with ruff extra selected
512
+ CONFTEST_CONTENT = """\
513
+ import pytest
514
+
515
+ from src.memory.history import clear
516
+
517
+
518
+ @pytest.fixture(autouse=True)
519
+ def reset_memory():
520
+ \"\"\"Reset conversation history before every test.\"\"\"
521
+ clear()
522
+ yield
523
+ clear()
524
+ """
525
+
526
+ TEST_CONTENT = """\
527
+ from unittest.mock import patch
528
+
529
+ from src.chatbot.chat import get_response
530
+
531
+
532
+ def test_get_response_returns_string():
533
+ with patch("src.chatbot.chat.get_llm_response", return_value="Hello!"):
534
+ with patch("src.chatbot.chat.get_system_prompt", return_value="You are helpful."):
535
+ result = get_response("Hi")
536
+ assert isinstance(result, str)
537
+ assert result == "Hello!"
538
+
539
+
540
+ def test_generate_response_mock():
541
+ with patch("src.chatbot.chat.get_llm_response", return_value="Mocked response"):
542
+ with patch("src.chatbot.chat.get_system_prompt", return_value="prompt"):
543
+ result = get_response("test input")
544
+ assert result == "Mocked response"
545
+
546
+
547
+ def test_get_response_non_empty():
548
+ with patch("src.chatbot.chat.get_llm_response", return_value="Sure!"):
549
+ with patch("src.chatbot.chat.get_system_prompt", return_value="prompt"):
550
+ result = get_response("Tell me something")
551
+ assert result != ""
552
+ """
553
+
554
+ # ─── README content ───────────────────────────────────────────────────────
555
+
556
+ def make_readme(framework: str, provider: str) -> str:
557
+ provider_key_map = {
558
+ "openai": "OPENAI_API_KEY=your-key",
559
+ "anthropic": "ANTHROPIC_API_KEY=your-key",
560
+ "gemini": "GOOGLE_API_KEY=your-key",
561
+ "openrouter": "OPENROUTER_API_KEY=your-key",
562
+ "ollama": "OLLAMA_BASE_URL=http://localhost:11434",
563
+ "groq": "GROQ_API_KEY=your-key",
564
+ }
565
+ key_line = provider_key_map.get(provider, "API_KEY=your-key")
566
+ return (
567
+ "# {project_name}\n\n"
568
+ f"An AI chatbot generated with Spawn, using {framework.title()} + {provider.title()}.\n\n"
569
+ "## Getting Started\n\n"
570
+ "1. Copy `.env.example` to `.env` and fill in your credentials:\n\n"
571
+ "```env\n"
572
+ f"{key_line}\n"
573
+ "```\n\n"
574
+ "2. Run the chatbot:\n\n"
575
+ "```bash\n"
576
+ "uv run python -m src.main\n"
577
+ "```\n\n"
578
+ "## Example\n\n"
579
+ "```\n"
580
+ "You: Hello\n"
581
+ "Bot: Hello! How can I help?\n"
582
+ "```\n\n"
583
+ "## Project Structure\n\n"
584
+ "```\n"
585
+ "{project_name}/\n"
586
+ "├── src/\n"
587
+ "│ ├── chatbot/ # Conversation orchestration\n"
588
+ "│ ├── providers/ # LLM provider (llm.py)\n"
589
+ "│ ├── prompts/ # system.txt\n"
590
+ "│ ├── memory/ # Runtime conversation history\n"
591
+ "│ ├── config/ # Settings and env loading\n"
592
+ "│ └── main.py\n"
593
+ "├── tests/\n"
594
+ "├── .env.example\n"
595
+ "└── README.md\n"
596
+ "```\n\n"
597
+ "## Running Tests\n\n"
598
+ "```bash\n"
599
+ "uv run pytest\n"
600
+ "```\n\n"
601
+ "## Roadmap (Not Yet Available)\n\n"
602
+ "```bash\n"
603
+ "spawn add rag\n"
604
+ "spawn add tools\n"
605
+ "spawn add vector-db\n"
606
+ "spawn add memory\n"
607
+ "spawn add mcp\n"
608
+ "spawn add voice\n"
609
+ "spawn add web-ui\n"
610
+ "spawn add streaming\n"
611
+ "spawn add observability\n"
612
+ "```\n"
613
+ )
614
+
615
+
616
+ def make_agents_md(framework: str, provider: str) -> str:
617
+ if framework == "litellm" and provider == "ollama":
618
+ key_name = "OLLAMA_API_BASE"
619
+ else:
620
+ provider_key_map = {
621
+ "openai": "OPENAI_API_KEY",
622
+ "anthropic": "ANTHROPIC_API_KEY",
623
+ "gemini": "GOOGLE_API_KEY",
624
+ "openrouter": "OPENROUTER_API_KEY",
625
+ "ollama": "OLLAMA_BASE_URL",
626
+ "groq": "GROQ_API_KEY",
627
+ }
628
+ key_name = provider_key_map.get(provider, "OPENAI_API_KEY")
629
+ return (
630
+ "# {project_name}\n\n"
631
+ f"AI chatbot using {framework.title()} with {provider.title()} provider.\n\n"
632
+ "## Required Environment Variable\n\n"
633
+ f"Set `{key_name}` in your `.env` file.\n"
634
+ )
635
+
636
+ # ─── GitHub Actions ───────────────────────────────────────────────────────
637
+
638
+ GITHUB_ACTIONS_CI_BASE = """\
639
+ name: CI
640
+
641
+ on:
642
+ push:
643
+ branches: [main]
644
+ pull_request:
645
+ branches: [main]
646
+
647
+ jobs:
648
+ ci:
649
+ runs-on: ubuntu-latest
650
+
651
+ steps:
652
+ - uses: actions/checkout@v4
653
+
654
+ - uses: astral-sh/setup-uv@v5
655
+
656
+ - name: Install dependencies
657
+ run: uv sync
658
+ """
659
+
660
+ GITHUB_ACTIONS_CI_RUFF_STEP = """\
661
+ - name: Lint
662
+ run: uv run ruff check .
663
+ """
664
+
665
+ GITHUB_ACTIONS_CI_PYTEST_STEP = """\
666
+ - name: Test
667
+ run: uv run pytest
668
+ """