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,463 @@
|
|
|
1
|
+
INIT_CONTENT = ""
|
|
2
|
+
|
|
3
|
+
# ─── Knowledge base ───────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
SAMPLE_KNOWLEDGE_CONTENT = """\
|
|
6
|
+
# Spawn Knowledge Base
|
|
7
|
+
|
|
8
|
+
## What is Spawn?
|
|
9
|
+
|
|
10
|
+
Spawn is an intent-based project generator that helps developers create
|
|
11
|
+
production-friendly project foundations in seconds. Instead of manually
|
|
12
|
+
creating folders, installing dependencies, and configuring tools, developers
|
|
13
|
+
run one command and get a complete, immediately runnable project.
|
|
14
|
+
|
|
15
|
+
## Spawn Intents
|
|
16
|
+
|
|
17
|
+
Spawn supports the following project intents:
|
|
18
|
+
|
|
19
|
+
- Backend API: FastAPI, Flask, or Django with production structure
|
|
20
|
+
- CLI Application: Typer, Click, or Argparse command-line tools
|
|
21
|
+
- Automation Tool: Workflow-based automation with logging
|
|
22
|
+
- AI Chatbot: Conversational AI with PydanticAI, OpenAI SDK, or LiteLLM
|
|
23
|
+
- AI Agent: Tool-calling agents with PydanticAI or OpenAI Agents SDK
|
|
24
|
+
- RAG System: Retrieval-Augmented Generation with LlamaIndex and ChromaDB
|
|
25
|
+
|
|
26
|
+
## How Spawn Works
|
|
27
|
+
|
|
28
|
+
Run `spawn create`, select your intent, choose your framework and provider,
|
|
29
|
+
pick any extras (ruff, pytest, github-actions), and Spawn generates the full
|
|
30
|
+
project structure, installs dependencies via uv, and shows you the exact
|
|
31
|
+
commands to run next. The entire setup takes under 30 seconds.
|
|
32
|
+
|
|
33
|
+
## RAG System Intent
|
|
34
|
+
|
|
35
|
+
The RAG System intent (added in v0.8.0) generates a complete
|
|
36
|
+
Retrieval-Augmented Generation application using LlamaIndex and ChromaDB.
|
|
37
|
+
It ingests documents from the data/ directory, creates embeddings using
|
|
38
|
+
OpenAI's text-embedding-3-small model, stores them in a local ChromaDB
|
|
39
|
+
vector database, and answers questions using retrieved context. On first
|
|
40
|
+
run the ingestion step is triggered automatically before the Q&A loop starts.
|
|
41
|
+
|
|
42
|
+
## Version History
|
|
43
|
+
|
|
44
|
+
- v0.6.0: Initial release with Backend API, CLI Application, Automation Tool,
|
|
45
|
+
and AI Chatbot intents.
|
|
46
|
+
- v0.7.0: Added AI Agent intent with PydanticAI and OpenAI Agents SDK support,
|
|
47
|
+
plus Groq provider across all AI intents.
|
|
48
|
+
- v0.8.0: Added RAG System intent with LlamaIndex, ChromaDB, and OpenAI.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# ─── Config ───────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
SETTINGS_CONTENT = """\
|
|
54
|
+
import os
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from dotenv import load_dotenv
|
|
58
|
+
from llama_index.core import Settings
|
|
59
|
+
from llama_index.embeddings.openai import OpenAIEmbedding
|
|
60
|
+
from llama_index.llms.openai import OpenAI
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_env() -> None:
|
|
64
|
+
load_dotenv()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def configure_llm() -> None:
|
|
68
|
+
api_key = os.getenv("OPENAI_API_KEY", "")
|
|
69
|
+
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
|
70
|
+
Settings.llm = OpenAI(model=model, api_key=api_key)
|
|
71
|
+
Settings.embed_model = OpenAIEmbedding(
|
|
72
|
+
model="text-embedding-3-small",
|
|
73
|
+
api_key=api_key,
|
|
74
|
+
)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# ─── Knowledge index ──────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
KNOWLEDGE_INDEX_CONTENT = """\
|
|
80
|
+
import re
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
import chromadb
|
|
84
|
+
from llama_index.core import StorageContext
|
|
85
|
+
from llama_index.vector_stores.chroma import ChromaVectorStore
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sanitize_collection_name(name: str) -> str:
|
|
89
|
+
cleaned = re.sub(r"[^a-zA-Z0-9._-]", "-", name)
|
|
90
|
+
cleaned = cleaned.strip("-._")
|
|
91
|
+
if len(cleaned) < 3:
|
|
92
|
+
cleaned = f"rag-{{cleaned}}".strip("-._")
|
|
93
|
+
if len(cleaned) < 3:
|
|
94
|
+
cleaned = "rag-collection"
|
|
95
|
+
return cleaned[:512]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
CHROMA_PATH = Path("chroma_db")
|
|
99
|
+
COLLECTION_NAME = _sanitize_collection_name("{project_name}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_vector_store() -> ChromaVectorStore:
|
|
103
|
+
client = chromadb.PersistentClient(path=str(CHROMA_PATH))
|
|
104
|
+
collection = client.get_or_create_collection(COLLECTION_NAME)
|
|
105
|
+
return ChromaVectorStore(chroma_collection=collection)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_storage_context() -> StorageContext:
|
|
109
|
+
return StorageContext.from_defaults(vector_store=get_vector_store())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def index_exists() -> bool:
|
|
113
|
+
if not CHROMA_PATH.exists():
|
|
114
|
+
return False
|
|
115
|
+
sqlite_file = CHROMA_PATH / "chroma.sqlite3"
|
|
116
|
+
return sqlite_file.exists() and sqlite_file.stat().st_size > 0
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# ─── Ingestion ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
INGESTION_CONTENT = """\
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
|
|
124
|
+
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
|
|
125
|
+
from openai import APIError, AuthenticationError, RateLimitError
|
|
126
|
+
|
|
127
|
+
from src.knowledge.index import get_storage_context
|
|
128
|
+
|
|
129
|
+
DATA_PATH = Path("data")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ingest_documents() -> VectorStoreIndex:
|
|
133
|
+
print("Ingesting documents from data/...")
|
|
134
|
+
documents = SimpleDirectoryReader(
|
|
135
|
+
input_dir=str(DATA_PATH),
|
|
136
|
+
required_exts=[".txt", ".md"],
|
|
137
|
+
recursive=True,
|
|
138
|
+
).load_data()
|
|
139
|
+
print(f"Loaded {{len(documents)}} document(s).")
|
|
140
|
+
storage_context = get_storage_context()
|
|
141
|
+
try:
|
|
142
|
+
index = VectorStoreIndex.from_documents(
|
|
143
|
+
documents,
|
|
144
|
+
storage_context=storage_context,
|
|
145
|
+
show_progress=True,
|
|
146
|
+
)
|
|
147
|
+
except AuthenticationError:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
"OpenAI rejected your API key. Check OPENAI_API_KEY in .env."
|
|
150
|
+
) from None
|
|
151
|
+
except RateLimitError as e:
|
|
152
|
+
if "insufficient_quota" in str(e):
|
|
153
|
+
raise RuntimeError(
|
|
154
|
+
"Your OpenAI account has no available quota/credits. "
|
|
155
|
+
"Check billing at https://platform.openai.com/settings/organization/billing"
|
|
156
|
+
) from None
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"OpenAI rate limit hit. Wait a moment and try again."
|
|
159
|
+
) from None
|
|
160
|
+
except APIError as e:
|
|
161
|
+
raise RuntimeError(f"OpenAI API error: {{e}}") from None
|
|
162
|
+
print("Ingestion complete. Index stored in chroma_db/")
|
|
163
|
+
return index
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
# ─── Retrieval ────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
RETRIEVAL_CONTENT = """\
|
|
169
|
+
from llama_index.core import VectorStoreIndex
|
|
170
|
+
from openai import APIError, AuthenticationError, RateLimitError
|
|
171
|
+
|
|
172
|
+
from src.knowledge.index import get_storage_context, get_vector_store
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def load_index() -> VectorStoreIndex:
|
|
176
|
+
vector_store = get_vector_store()
|
|
177
|
+
storage_context = get_storage_context()
|
|
178
|
+
return VectorStoreIndex.from_vector_store(
|
|
179
|
+
vector_store,
|
|
180
|
+
storage_context=storage_context,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def answer_question(question: str) -> str:
|
|
185
|
+
index = load_index()
|
|
186
|
+
query_engine = index.as_query_engine(
|
|
187
|
+
similarity_top_k=3,
|
|
188
|
+
)
|
|
189
|
+
try:
|
|
190
|
+
response = query_engine.query(question)
|
|
191
|
+
except AuthenticationError:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"OpenAI rejected your API key. Check OPENAI_API_KEY in .env."
|
|
194
|
+
) from None
|
|
195
|
+
except RateLimitError as e:
|
|
196
|
+
if "insufficient_quota" in str(e):
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
"Your OpenAI account has no available quota/credits. "
|
|
199
|
+
"Check billing at https://platform.openai.com/settings/organization/billing"
|
|
200
|
+
) from None
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
"OpenAI rate limit hit. Wait a moment and try again."
|
|
203
|
+
) from None
|
|
204
|
+
except APIError as e:
|
|
205
|
+
raise RuntimeError(f"OpenAI API error: {{e}}") from None
|
|
206
|
+
return str(response)
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# ─── Main ─────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
MAIN_CONTENT = """\
|
|
212
|
+
from src.config.settings import configure_llm, load_env
|
|
213
|
+
from src.ingestion.ingest import ingest_documents
|
|
214
|
+
from src.knowledge.index import index_exists
|
|
215
|
+
from src.retrieval.retrieve import answer_question
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main() -> None:
|
|
219
|
+
load_env()
|
|
220
|
+
configure_llm()
|
|
221
|
+
print("{project_name} RAG System")
|
|
222
|
+
print("-" * 40)
|
|
223
|
+
if not index_exists():
|
|
224
|
+
print("No index found. Running ingestion...\\n")
|
|
225
|
+
try:
|
|
226
|
+
ingest_documents()
|
|
227
|
+
except RuntimeError as e:
|
|
228
|
+
print(f"\\n[Error] {{e}}\\n")
|
|
229
|
+
import shutil
|
|
230
|
+
from pathlib import Path
|
|
231
|
+
shutil.rmtree(Path("chroma_db"), ignore_errors=True)
|
|
232
|
+
return
|
|
233
|
+
print()
|
|
234
|
+
print("Ask a question (or type 'quit' to exit):\\n")
|
|
235
|
+
while True:
|
|
236
|
+
question = input("You: ").strip()
|
|
237
|
+
if not question:
|
|
238
|
+
continue
|
|
239
|
+
if question.lower() in ("quit", "exit"):
|
|
240
|
+
break
|
|
241
|
+
try:
|
|
242
|
+
answer = answer_question(question)
|
|
243
|
+
print(f"\\nAnswer: {{answer}}\\n")
|
|
244
|
+
except RuntimeError as e:
|
|
245
|
+
print(f"\\n[Error] {{e}}\\n")
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
main()
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
# ─── Tests ────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
CONFTEST_CONTENT = """\
|
|
256
|
+
# conftest.py — shared test fixtures for RAG tests
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
TEST_CONTENT = """\
|
|
260
|
+
from pathlib import Path
|
|
261
|
+
from unittest.mock import MagicMock, patch
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_index_exists_false_when_no_dir(tmp_path, monkeypatch):
|
|
265
|
+
monkeypatch.chdir(tmp_path)
|
|
266
|
+
from src.knowledge.index import index_exists
|
|
267
|
+
assert not index_exists()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_index_exists_false_when_empty_dir(tmp_path, monkeypatch):
|
|
271
|
+
monkeypatch.chdir(tmp_path)
|
|
272
|
+
(tmp_path / "chroma_db").mkdir()
|
|
273
|
+
from src.knowledge.index import index_exists
|
|
274
|
+
assert not index_exists()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_index_exists_true_when_has_file(tmp_path, monkeypatch):
|
|
278
|
+
monkeypatch.chdir(tmp_path)
|
|
279
|
+
chroma = tmp_path / "chroma_db"
|
|
280
|
+
chroma.mkdir()
|
|
281
|
+
(chroma / "chroma.sqlite3").write_text("db")
|
|
282
|
+
from src.knowledge.index import index_exists
|
|
283
|
+
assert index_exists()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_index_exists_false_when_sqlite_file_empty(tmp_path, monkeypatch):
|
|
287
|
+
monkeypatch.chdir(tmp_path)
|
|
288
|
+
chroma = tmp_path / "chroma_db"
|
|
289
|
+
chroma.mkdir()
|
|
290
|
+
(chroma / "chroma.sqlite3").write_bytes(b"")
|
|
291
|
+
from src.knowledge.index import index_exists
|
|
292
|
+
assert not index_exists()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_sample_knowledge_file_exists():
|
|
296
|
+
data_path = Path("data") / "sample_knowledge.md"
|
|
297
|
+
assert data_path.exists(), "data/sample_knowledge.md must exist"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_answer_question_mock():
|
|
301
|
+
mock_engine = MagicMock()
|
|
302
|
+
mock_engine.query.return_value = "Spawn is a project generator."
|
|
303
|
+
mock_index = MagicMock()
|
|
304
|
+
mock_index.as_query_engine.return_value = mock_engine
|
|
305
|
+
|
|
306
|
+
with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
|
|
307
|
+
from src.retrieval.retrieve import answer_question
|
|
308
|
+
result = answer_question("What is Spawn?")
|
|
309
|
+
|
|
310
|
+
assert isinstance(result, str)
|
|
311
|
+
assert len(result) > 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_answer_question_rate_limit_raises_runtime_error():
|
|
315
|
+
from openai import RateLimitError
|
|
316
|
+
|
|
317
|
+
mock_engine = MagicMock()
|
|
318
|
+
mock_response = MagicMock()
|
|
319
|
+
mock_response.status_code = 429
|
|
320
|
+
mock_engine.query.side_effect = RateLimitError(
|
|
321
|
+
"insufficient_quota", response=mock_response, body=None
|
|
322
|
+
)
|
|
323
|
+
mock_index = MagicMock()
|
|
324
|
+
mock_index.as_query_engine.return_value = mock_engine
|
|
325
|
+
|
|
326
|
+
with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
|
|
327
|
+
from src.retrieval.retrieve import answer_question
|
|
328
|
+
try:
|
|
329
|
+
answer_question("What is Spawn?")
|
|
330
|
+
assert False, "Expected RuntimeError"
|
|
331
|
+
except RuntimeError as e:
|
|
332
|
+
assert "quota" in str(e).lower() or "rate limit" in str(e).lower()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_answer_question_api_error_raises_runtime_error():
|
|
336
|
+
from openai import APIError
|
|
337
|
+
|
|
338
|
+
mock_engine = MagicMock()
|
|
339
|
+
mock_response = MagicMock()
|
|
340
|
+
mock_response.status_code = 500
|
|
341
|
+
mock_engine.query.side_effect = APIError(
|
|
342
|
+
"server error", response=mock_response, body=None
|
|
343
|
+
)
|
|
344
|
+
mock_index = MagicMock()
|
|
345
|
+
mock_index.as_query_engine.return_value = mock_engine
|
|
346
|
+
|
|
347
|
+
with patch("src.retrieval.retrieve.load_index", return_value=mock_index):
|
|
348
|
+
from src.retrieval.retrieve import answer_question
|
|
349
|
+
try:
|
|
350
|
+
answer_question("What is Spawn?")
|
|
351
|
+
assert False, "Expected RuntimeError"
|
|
352
|
+
except RuntimeError as e:
|
|
353
|
+
assert "OpenAI API error" in str(e)
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
# ─── Env ──────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
ENV_CONTENT = """\
|
|
359
|
+
APP_NAME={project_name}
|
|
360
|
+
OPENAI_API_KEY=
|
|
361
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
# ─── README ───────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def make_readme() -> str:
|
|
368
|
+
return (
|
|
369
|
+
"# {project_name}\n\n"
|
|
370
|
+
"A RAG system generated with Spawn using LlamaIndex and ChromaDB.\n\n"
|
|
371
|
+
"## Getting Started\n\n"
|
|
372
|
+
"1. Rename `.env.example` to `.env`\n\n"
|
|
373
|
+
"2. Add your `OPENAI_API_KEY` to `.env`:\n\n"
|
|
374
|
+
"```env\n"
|
|
375
|
+
"OPENAI_API_KEY=your-key\n"
|
|
376
|
+
"```\n\n"
|
|
377
|
+
"3. (Optional) Add your own `.txt` or `.md` files to `data/`\n\n"
|
|
378
|
+
"4. Run the RAG system:\n\n"
|
|
379
|
+
"```bash\n"
|
|
380
|
+
"uv run python -m src.main\n"
|
|
381
|
+
"```\n\n"
|
|
382
|
+
"On first run, documents in `data/` are automatically ingested.\n"
|
|
383
|
+
"Then ask questions about your knowledge base.\n\n"
|
|
384
|
+
"## Example\n\n"
|
|
385
|
+
"```\n"
|
|
386
|
+
"You: What is Spawn?\n"
|
|
387
|
+
"Answer: Spawn is an intent-based project generator...\n"
|
|
388
|
+
"```\n\n"
|
|
389
|
+
"## Adding Knowledge\n\n"
|
|
390
|
+
"Drop `.txt` or `.md` files into `data/` and delete `chroma_db/` to re-index:\n\n"
|
|
391
|
+
"```bash\n"
|
|
392
|
+
"rm -rf chroma_db/\n"
|
|
393
|
+
"uv run python -m src.main\n"
|
|
394
|
+
"```\n\n"
|
|
395
|
+
"## Project Structure\n\n"
|
|
396
|
+
"```\n"
|
|
397
|
+
"{project_name}/\n"
|
|
398
|
+
"├── data/ # Knowledge documents\n"
|
|
399
|
+
"│ └── sample_knowledge.md\n"
|
|
400
|
+
"├── src/\n"
|
|
401
|
+
"│ ├── config/ # Settings and LLM configuration\n"
|
|
402
|
+
"│ ├── knowledge/ # ChromaDB index management\n"
|
|
403
|
+
"│ ├── ingestion/ # Document ingestion pipeline\n"
|
|
404
|
+
"│ ├── retrieval/ # Query engine and retrieval\n"
|
|
405
|
+
"│ └── main.py\n"
|
|
406
|
+
"├── tests/\n"
|
|
407
|
+
"├── chroma_db/ # Created on first run\n"
|
|
408
|
+
"├── .env.example\n"
|
|
409
|
+
"└── README.md\n"
|
|
410
|
+
"```\n\n"
|
|
411
|
+
"## Running Tests\n\n"
|
|
412
|
+
"```bash\n"
|
|
413
|
+
"uv run pytest\n"
|
|
414
|
+
"```\n"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def make_agents_md() -> str:
|
|
419
|
+
return (
|
|
420
|
+
"# {project_name}\n\n"
|
|
421
|
+
"A RAG system generated with Spawn using LlamaIndex and ChromaDB.\n\n"
|
|
422
|
+
"## Required Environment Variable\n\n"
|
|
423
|
+
"Set `OPENAI_API_KEY` in your `.env` file to run this system:\n\n"
|
|
424
|
+
"```env\n"
|
|
425
|
+
"OPENAI_API_KEY=your-key\n"
|
|
426
|
+
"```\n\n"
|
|
427
|
+
"## Ingestion\n\n"
|
|
428
|
+
"Documents placed in `data/` are automatically ingested into `chroma_db/` on first run.\n"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# ─── GitHub Actions ───────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
GITHUB_ACTIONS_CI_BASE = """\
|
|
434
|
+
name: CI
|
|
435
|
+
|
|
436
|
+
on:
|
|
437
|
+
push:
|
|
438
|
+
branches: [main]
|
|
439
|
+
pull_request:
|
|
440
|
+
branches: [main]
|
|
441
|
+
|
|
442
|
+
jobs:
|
|
443
|
+
ci:
|
|
444
|
+
runs-on: ubuntu-latest
|
|
445
|
+
|
|
446
|
+
steps:
|
|
447
|
+
- uses: actions/checkout@v4
|
|
448
|
+
|
|
449
|
+
- uses: astral-sh/setup-uv@v5
|
|
450
|
+
|
|
451
|
+
- name: Install dependencies
|
|
452
|
+
run: uv sync
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
GITHUB_ACTIONS_CI_RUFF_STEP = """\
|
|
456
|
+
- name: Lint
|
|
457
|
+
run: uv run ruff check .
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP = """\
|
|
461
|
+
- name: Test
|
|
462
|
+
run: uv run pytest
|
|
463
|
+
"""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
README_CONTENT = """# {project_name}
|
|
2
|
+
|
|
3
|
+
Project generated with Spawn.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
GITIGNORE_CONTENT = """# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.pyo
|
|
10
|
+
*.pyd
|
|
11
|
+
*.pyc
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Distribution / packaging
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
*.egg
|
|
23
|
+
|
|
24
|
+
# uv
|
|
25
|
+
.uv/
|
|
26
|
+
|
|
27
|
+
# Environment variables
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
|
|
31
|
+
# IDEs
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.iml
|
|
35
|
+
|
|
36
|
+
# OS
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
|
|
40
|
+
# Testing
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
.coverage
|
|
43
|
+
htmlcov/
|
|
44
|
+
|
|
45
|
+
# Mypy
|
|
46
|
+
.mypy_cache/
|
|
47
|
+
|
|
48
|
+
# Ruff
|
|
49
|
+
.ruff_cache/
|
|
50
|
+
|
|
51
|
+
# Spawn metadata
|
|
52
|
+
.spawn/
|
|
53
|
+
|
|
54
|
+
# Logs
|
|
55
|
+
logs/*.log
|
|
56
|
+
|
|
57
|
+
# ChromaDB vector store (regenerate with: delete chroma_db/ and re-run)
|
|
58
|
+
chroma_db/
|
|
59
|
+
!chroma_db/.gitkeep
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
AGENTS_MD_CONTENT = """# Agent Context: {project_name}
|
|
63
|
+
|
|
64
|
+
This file orients coding agents (Claude Code, and others that read
|
|
65
|
+
`AGENTS.md`) working in this repository.
|
|
66
|
+
|
|
67
|
+
## Project structure
|
|
68
|
+
|
|
69
|
+
See the folder layout in `README.md`. This project was generated
|
|
70
|
+
with Spawn.
|
|
71
|
+
|
|
72
|
+
## Setup
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Running tests
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run pytest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Conventions
|
|
85
|
+
|
|
86
|
+
- Dependencies are managed with uv, not pip directly.
|
|
87
|
+
- Run `uv run ruff check .` before committing, if Ruff is configured
|
|
88
|
+
for this project (see `pyproject.toml`).
|
|
89
|
+
"""
|
spawn/utils/__init__.py
ADDED
|
File without changes
|
spawn/utils/banner.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from rich.console import Group
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
from spawn.utils.console import console
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
LINES = [
|
|
9
|
+
" ███████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗",
|
|
10
|
+
" ██╔════╝██╔══██╗██╔══██╗██║ ██║████╗ ██║",
|
|
11
|
+
" ███████╗██████╔╝███████║██║ █╗ ██║██╔██╗██║",
|
|
12
|
+
" ╚════██║██╔═══╝ ██╔══██║██║███╗██║██║╚████║",
|
|
13
|
+
" ███████║██║ ██║ ██║╚███╔███╔╝██║ ╚███║",
|
|
14
|
+
" ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚══╝",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
COLORS = ["#B9D6F2", "#94B8D9", "#6F9AC0", "#4A7CA7", "#2D5E8E", "#0F2D52"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def show_banner() -> None:
|
|
21
|
+
logo_lines = []
|
|
22
|
+
for line_text, color in zip(LINES, COLORS):
|
|
23
|
+
t = Text(line_text)
|
|
24
|
+
t.stylize(f"bold {color}")
|
|
25
|
+
logo_lines.append(t)
|
|
26
|
+
|
|
27
|
+
subtitle = Text(" ✿ scaffold your next project", style="dim #94B8D9")
|
|
28
|
+
logo_group = Group(*logo_lines, subtitle)
|
|
29
|
+
|
|
30
|
+
console.print()
|
|
31
|
+
console.print(
|
|
32
|
+
Panel(
|
|
33
|
+
logo_group,
|
|
34
|
+
border_style="#2D5E8E",
|
|
35
|
+
padding=(1, 4),
|
|
36
|
+
expand=False,
|
|
37
|
+
title="[#94B8D9]◆[/#94B8D9]",
|
|
38
|
+
title_align="center",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
console.print()
|
spawn/utils/console.py
ADDED