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.
- spawn/__init__.py +6 -0
- spawn/cli/__init__.py +0 -0
- spawn/cli/app.py +218 -0
- spawn/cli/noninteractive.py +204 -0
- spawn/cli/prompts.py +267 -0
- spawn/core/__init__.py +0 -0
- spawn/core/exceptions.py +7 -0
- spawn/core/models.py +21 -0
- spawn/core/registry.py +152 -0
- spawn/generators/__init__.py +0 -0
- spawn/generators/custom_structure.py +508 -0
- spawn/generators/project_generator.py +115 -0
- spawn/github/__init__.py +0 -0
- spawn/github/exceptions.py +5 -0
- spawn/github/publisher.py +69 -0
- spawn/github/validators.py +23 -0
- spawn/templates/__init__.py +0 -0
- spawn/templates/agent/__init__.py +182 -0
- spawn/templates/agent/content.py +374 -0
- spawn/templates/automation/__init__.py +100 -0
- spawn/templates/automation/content.py +181 -0
- spawn/templates/backend_api/__init__.py +243 -0
- spawn/templates/backend_api/content.py +509 -0
- spawn/templates/base.py +58 -0
- spawn/templates/chatbot/__init__.py +243 -0
- spawn/templates/chatbot/content.py +668 -0
- spawn/templates/cli_application/__init__.py +211 -0
- spawn/templates/cli_application/content.py +512 -0
- spawn/templates/data_project/__init__.py +125 -0
- spawn/templates/data_project/analysis/__init__.py +74 -0
- spawn/templates/data_project/analysis/content.py +190 -0
- spawn/templates/data_project/dashboard/__init__.py +61 -0
- spawn/templates/data_project/dashboard/content.py +135 -0
- spawn/templates/data_project/etl/__init__.py +66 -0
- spawn/templates/data_project/etl/content.py +239 -0
- spawn/templates/data_project/ml/__init__.py +62 -0
- spawn/templates/data_project/ml/content.py +239 -0
- spawn/templates/mcp_server/__init__.py +83 -0
- spawn/templates/mcp_server/content.py +161 -0
- spawn/templates/rag/__init__.py +124 -0
- spawn/templates/rag/content.py +463 -0
- spawn/templates/shared_content.py +89 -0
- spawn/utils/__init__.py +0 -0
- spawn/utils/banner.py +41 -0
- spawn/utils/console.py +3 -0
- spawn/utils/doctor.py +594 -0
- spawn/utils/git.py +132 -0
- spawn/utils/success.py +47 -0
- spawn/utils/theme.py +25 -0
- spawn/utils/uv.py +61 -0
- spawn/utils/validators.py +15 -0
- spawnio-1.0.5.dist-info/METADATA +798 -0
- spawnio-1.0.5.dist-info/RECORD +56 -0
- spawnio-1.0.5.dist-info/WHEEL +4 -0
- spawnio-1.0.5.dist-info/entry_points.txt +2 -0
- 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
|
+
"""
|