nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Agent for NC1709
|
|
3
|
+
Scaffolds FastAPI projects, endpoints, models, and more
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from ..base import (
|
|
12
|
+
Plugin, PluginMetadata, PluginCapability,
|
|
13
|
+
ActionResult
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EndpointInfo:
|
|
19
|
+
"""Represents an API endpoint"""
|
|
20
|
+
method: str
|
|
21
|
+
path: str
|
|
22
|
+
function_name: str
|
|
23
|
+
description: str
|
|
24
|
+
request_model: Optional[str] = None
|
|
25
|
+
response_model: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FastAPIAgent(Plugin):
|
|
29
|
+
"""
|
|
30
|
+
FastAPI scaffolding and development agent.
|
|
31
|
+
|
|
32
|
+
Provides:
|
|
33
|
+
- Project scaffolding with best practices
|
|
34
|
+
- Endpoint generation
|
|
35
|
+
- Pydantic model generation
|
|
36
|
+
- CRUD operations scaffolding
|
|
37
|
+
- Database integration (SQLAlchemy)
|
|
38
|
+
- Authentication scaffolding
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
METADATA = PluginMetadata(
|
|
42
|
+
name="fastapi",
|
|
43
|
+
version="1.0.0",
|
|
44
|
+
description="FastAPI project scaffolding and development",
|
|
45
|
+
author="NC1709 Team",
|
|
46
|
+
capabilities=[
|
|
47
|
+
PluginCapability.CODE_GENERATION,
|
|
48
|
+
PluginCapability.PROJECT_SCAFFOLDING
|
|
49
|
+
],
|
|
50
|
+
keywords=[
|
|
51
|
+
"fastapi", "api", "rest", "endpoint", "pydantic",
|
|
52
|
+
"model", "schema", "crud", "router", "uvicorn",
|
|
53
|
+
"async", "python", "backend"
|
|
54
|
+
],
|
|
55
|
+
config_schema={
|
|
56
|
+
"project_path": {"type": "string", "default": "."},
|
|
57
|
+
"use_async": {"type": "boolean", "default": True},
|
|
58
|
+
"database": {"type": "string", "enum": ["sqlite", "postgresql", "mysql", "none"], "default": "sqlite"}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def metadata(self) -> PluginMetadata:
|
|
64
|
+
return self.METADATA
|
|
65
|
+
|
|
66
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
67
|
+
super().__init__(config)
|
|
68
|
+
self._project_path: Optional[Path] = None
|
|
69
|
+
|
|
70
|
+
def initialize(self) -> bool:
|
|
71
|
+
"""Initialize the FastAPI agent"""
|
|
72
|
+
self._project_path = Path(self._config.get("project_path", ".")).resolve()
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def cleanup(self) -> None:
|
|
76
|
+
"""Cleanup resources"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def _register_actions(self) -> None:
|
|
80
|
+
"""Register FastAPI actions"""
|
|
81
|
+
self.register_action(
|
|
82
|
+
"scaffold",
|
|
83
|
+
self.scaffold_project,
|
|
84
|
+
"Create a new FastAPI project structure",
|
|
85
|
+
parameters={
|
|
86
|
+
"name": {"type": "string", "required": True},
|
|
87
|
+
"with_db": {"type": "boolean", "default": True},
|
|
88
|
+
"with_auth": {"type": "boolean", "default": False}
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.register_action(
|
|
93
|
+
"endpoint",
|
|
94
|
+
self.create_endpoint,
|
|
95
|
+
"Generate a new API endpoint",
|
|
96
|
+
parameters={
|
|
97
|
+
"path": {"type": "string", "required": True},
|
|
98
|
+
"method": {"type": "string", "default": "GET"},
|
|
99
|
+
"name": {"type": "string", "required": True}
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.register_action(
|
|
104
|
+
"model",
|
|
105
|
+
self.create_model,
|
|
106
|
+
"Generate a Pydantic model",
|
|
107
|
+
parameters={
|
|
108
|
+
"name": {"type": "string", "required": True},
|
|
109
|
+
"fields": {"type": "object", "required": True}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.register_action(
|
|
114
|
+
"crud",
|
|
115
|
+
self.create_crud,
|
|
116
|
+
"Generate CRUD endpoints for a model",
|
|
117
|
+
parameters={
|
|
118
|
+
"model": {"type": "string", "required": True},
|
|
119
|
+
"prefix": {"type": "string", "default": ""}
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.register_action(
|
|
124
|
+
"router",
|
|
125
|
+
self.create_router,
|
|
126
|
+
"Generate a new router module",
|
|
127
|
+
parameters={
|
|
128
|
+
"name": {"type": "string", "required": True},
|
|
129
|
+
"prefix": {"type": "string", "default": ""}
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.register_action(
|
|
134
|
+
"analyze",
|
|
135
|
+
self.analyze_project,
|
|
136
|
+
"Analyze existing FastAPI project structure"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def scaffold_project(
|
|
140
|
+
self,
|
|
141
|
+
name: str,
|
|
142
|
+
with_db: bool = True,
|
|
143
|
+
with_auth: bool = False
|
|
144
|
+
) -> ActionResult:
|
|
145
|
+
"""Create a new FastAPI project structure
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
name: Project name
|
|
149
|
+
with_db: Include database setup
|
|
150
|
+
with_auth: Include authentication
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ActionResult
|
|
154
|
+
"""
|
|
155
|
+
project_dir = self._project_path / name
|
|
156
|
+
|
|
157
|
+
if project_dir.exists():
|
|
158
|
+
return ActionResult.fail(f"Directory '{name}' already exists")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Create directory structure
|
|
162
|
+
dirs = [
|
|
163
|
+
project_dir,
|
|
164
|
+
project_dir / "app",
|
|
165
|
+
project_dir / "app" / "api",
|
|
166
|
+
project_dir / "app" / "api" / "v1",
|
|
167
|
+
project_dir / "app" / "core",
|
|
168
|
+
project_dir / "app" / "models",
|
|
169
|
+
project_dir / "app" / "schemas",
|
|
170
|
+
project_dir / "app" / "services",
|
|
171
|
+
project_dir / "tests",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
if with_db:
|
|
175
|
+
dirs.append(project_dir / "app" / "db")
|
|
176
|
+
dirs.append(project_dir / "alembic")
|
|
177
|
+
|
|
178
|
+
for d in dirs:
|
|
179
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
# Create main.py
|
|
182
|
+
main_content = self._generate_main(name, with_db, with_auth)
|
|
183
|
+
(project_dir / "app" / "main.py").write_text(main_content)
|
|
184
|
+
|
|
185
|
+
# Create __init__.py files
|
|
186
|
+
for d in dirs:
|
|
187
|
+
if "app" in str(d) or "tests" in str(d):
|
|
188
|
+
init_file = d / "__init__.py"
|
|
189
|
+
if not init_file.exists():
|
|
190
|
+
init_file.write_text("")
|
|
191
|
+
|
|
192
|
+
# Create config
|
|
193
|
+
config_content = self._generate_config(name, with_db)
|
|
194
|
+
(project_dir / "app" / "core" / "config.py").write_text(config_content)
|
|
195
|
+
|
|
196
|
+
# Create requirements.txt
|
|
197
|
+
requirements = self._generate_requirements(with_db, with_auth)
|
|
198
|
+
(project_dir / "requirements.txt").write_text(requirements)
|
|
199
|
+
|
|
200
|
+
# Create .env.example
|
|
201
|
+
env_content = self._generate_env_example(name, with_db)
|
|
202
|
+
(project_dir / ".env.example").write_text(env_content)
|
|
203
|
+
|
|
204
|
+
# Create example router
|
|
205
|
+
router_content = self._generate_example_router()
|
|
206
|
+
(project_dir / "app" / "api" / "v1" / "health.py").write_text(router_content)
|
|
207
|
+
|
|
208
|
+
# Create API init
|
|
209
|
+
api_init = self._generate_api_init()
|
|
210
|
+
(project_dir / "app" / "api" / "v1" / "__init__.py").write_text(api_init)
|
|
211
|
+
|
|
212
|
+
if with_db:
|
|
213
|
+
# Create database setup
|
|
214
|
+
db_content = self._generate_db_setup()
|
|
215
|
+
(project_dir / "app" / "db" / "database.py").write_text(db_content)
|
|
216
|
+
|
|
217
|
+
# Create base model
|
|
218
|
+
base_model = self._generate_base_model()
|
|
219
|
+
(project_dir / "app" / "models" / "base.py").write_text(base_model)
|
|
220
|
+
|
|
221
|
+
if with_auth:
|
|
222
|
+
# Create auth module
|
|
223
|
+
auth_content = self._generate_auth_module()
|
|
224
|
+
(project_dir / "app" / "core" / "auth.py").write_text(auth_content)
|
|
225
|
+
|
|
226
|
+
files_created = len(list(project_dir.rglob("*")))
|
|
227
|
+
|
|
228
|
+
return ActionResult.ok(
|
|
229
|
+
message=f"Created FastAPI project '{name}' with {files_created} files",
|
|
230
|
+
data={
|
|
231
|
+
"project_path": str(project_dir),
|
|
232
|
+
"with_db": with_db,
|
|
233
|
+
"with_auth": with_auth,
|
|
234
|
+
"next_steps": [
|
|
235
|
+
f"cd {name}",
|
|
236
|
+
"python -m venv venv",
|
|
237
|
+
"source venv/bin/activate",
|
|
238
|
+
"pip install -r requirements.txt",
|
|
239
|
+
"uvicorn app.main:app --reload"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return ActionResult.fail(str(e))
|
|
246
|
+
|
|
247
|
+
def create_endpoint(
|
|
248
|
+
self,
|
|
249
|
+
path: str,
|
|
250
|
+
method: str = "GET",
|
|
251
|
+
name: str = ""
|
|
252
|
+
) -> ActionResult:
|
|
253
|
+
"""Generate a new API endpoint
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
path: URL path (e.g., "/users/{user_id}")
|
|
257
|
+
method: HTTP method
|
|
258
|
+
name: Function name
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ActionResult with generated code
|
|
262
|
+
"""
|
|
263
|
+
method = method.upper()
|
|
264
|
+
if method not in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
|
265
|
+
return ActionResult.fail(f"Invalid HTTP method: {method}")
|
|
266
|
+
|
|
267
|
+
# Generate function name if not provided
|
|
268
|
+
if not name:
|
|
269
|
+
name = self._path_to_function_name(path, method)
|
|
270
|
+
|
|
271
|
+
# Detect path parameters
|
|
272
|
+
path_params = re.findall(r'\{(\w+)\}', path)
|
|
273
|
+
|
|
274
|
+
# Generate endpoint code
|
|
275
|
+
code = self._generate_endpoint_code(path, method, name, path_params)
|
|
276
|
+
|
|
277
|
+
return ActionResult.ok(
|
|
278
|
+
message=f"Generated {method} {path} endpoint",
|
|
279
|
+
data={
|
|
280
|
+
"code": code,
|
|
281
|
+
"function_name": name,
|
|
282
|
+
"path_params": path_params
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def create_model(
|
|
287
|
+
self,
|
|
288
|
+
name: str,
|
|
289
|
+
fields: Dict[str, str]
|
|
290
|
+
) -> ActionResult:
|
|
291
|
+
"""Generate a Pydantic model
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
name: Model name
|
|
295
|
+
fields: Dict of field_name -> field_type
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
ActionResult with generated code
|
|
299
|
+
"""
|
|
300
|
+
# Generate model code
|
|
301
|
+
code = self._generate_pydantic_model(name, fields)
|
|
302
|
+
|
|
303
|
+
return ActionResult.ok(
|
|
304
|
+
message=f"Generated Pydantic model '{name}'",
|
|
305
|
+
data={"code": code, "fields": list(fields.keys())}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def create_crud(
|
|
309
|
+
self,
|
|
310
|
+
model: str,
|
|
311
|
+
prefix: str = ""
|
|
312
|
+
) -> ActionResult:
|
|
313
|
+
"""Generate CRUD endpoints for a model
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
model: Model name
|
|
317
|
+
prefix: URL prefix
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
ActionResult with generated code
|
|
321
|
+
"""
|
|
322
|
+
model_lower = model.lower()
|
|
323
|
+
prefix = prefix or f"/{model_lower}s"
|
|
324
|
+
|
|
325
|
+
code = self._generate_crud_router(model, prefix)
|
|
326
|
+
|
|
327
|
+
return ActionResult.ok(
|
|
328
|
+
message=f"Generated CRUD router for '{model}'",
|
|
329
|
+
data={
|
|
330
|
+
"code": code,
|
|
331
|
+
"endpoints": [
|
|
332
|
+
f"GET {prefix}",
|
|
333
|
+
f"POST {prefix}",
|
|
334
|
+
f"GET {prefix}/{{id}}",
|
|
335
|
+
f"PUT {prefix}/{{id}}",
|
|
336
|
+
f"DELETE {prefix}/{{id}}"
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def create_router(
|
|
342
|
+
self,
|
|
343
|
+
name: str,
|
|
344
|
+
prefix: str = ""
|
|
345
|
+
) -> ActionResult:
|
|
346
|
+
"""Generate a new router module
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: Router name
|
|
350
|
+
prefix: URL prefix
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
ActionResult with generated code
|
|
354
|
+
"""
|
|
355
|
+
prefix = prefix or f"/{name}"
|
|
356
|
+
|
|
357
|
+
code = f'''"""
|
|
358
|
+
{name.title()} Router
|
|
359
|
+
"""
|
|
360
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
361
|
+
|
|
362
|
+
router = APIRouter(prefix="{prefix}", tags=["{name}"])
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@router.get("/")
|
|
366
|
+
async def list_{name}():
|
|
367
|
+
"""List all {name}"""
|
|
368
|
+
return {{"message": "List {name}"}}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@router.get("/{{item_id}}")
|
|
372
|
+
async def get_{name}(item_id: int):
|
|
373
|
+
"""Get a single {name}"""
|
|
374
|
+
return {{"id": item_id}}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@router.post("/", status_code=status.HTTP_201_CREATED)
|
|
378
|
+
async def create_{name}():
|
|
379
|
+
"""Create a new {name}"""
|
|
380
|
+
return {{"message": "Created"}}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@router.put("/{{item_id}}")
|
|
384
|
+
async def update_{name}(item_id: int):
|
|
385
|
+
"""Update a {name}"""
|
|
386
|
+
return {{"id": item_id, "updated": True}}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@router.delete("/{{item_id}}", status_code=status.HTTP_204_NO_CONTENT)
|
|
390
|
+
async def delete_{name}(item_id: int):
|
|
391
|
+
"""Delete a {name}"""
|
|
392
|
+
return None
|
|
393
|
+
'''
|
|
394
|
+
|
|
395
|
+
return ActionResult.ok(
|
|
396
|
+
message=f"Generated router '{name}'",
|
|
397
|
+
data={"code": code, "prefix": prefix}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def analyze_project(self) -> ActionResult:
|
|
401
|
+
"""Analyze existing FastAPI project structure
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
ActionResult with project analysis
|
|
405
|
+
"""
|
|
406
|
+
# Look for FastAPI indicators
|
|
407
|
+
app_dir = self._project_path / "app"
|
|
408
|
+
main_file = self._project_path / "main.py"
|
|
409
|
+
app_main = app_dir / "main.py" if app_dir.exists() else None
|
|
410
|
+
|
|
411
|
+
if not (main_file.exists() or (app_main and app_main.exists())):
|
|
412
|
+
return ActionResult.fail("No FastAPI project found in current directory")
|
|
413
|
+
|
|
414
|
+
analysis = {
|
|
415
|
+
"project_path": str(self._project_path),
|
|
416
|
+
"structure": [],
|
|
417
|
+
"routers": [],
|
|
418
|
+
"models": [],
|
|
419
|
+
"endpoints": []
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Scan for Python files
|
|
423
|
+
for py_file in self._project_path.rglob("*.py"):
|
|
424
|
+
rel_path = py_file.relative_to(self._project_path)
|
|
425
|
+
analysis["structure"].append(str(rel_path))
|
|
426
|
+
|
|
427
|
+
# Check for routers
|
|
428
|
+
content = py_file.read_text()
|
|
429
|
+
if "APIRouter" in content:
|
|
430
|
+
analysis["routers"].append(str(rel_path))
|
|
431
|
+
|
|
432
|
+
# Check for models
|
|
433
|
+
if "BaseModel" in content and "pydantic" in content.lower():
|
|
434
|
+
analysis["models"].append(str(rel_path))
|
|
435
|
+
|
|
436
|
+
# Find endpoints
|
|
437
|
+
for match in re.finditer(r'@\w+\.(get|post|put|delete|patch)\(["\']([^"\']+)', content):
|
|
438
|
+
analysis["endpoints"].append({
|
|
439
|
+
"file": str(rel_path),
|
|
440
|
+
"method": match.group(1).upper(),
|
|
441
|
+
"path": match.group(2)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
return ActionResult.ok(
|
|
445
|
+
message=f"Analyzed project with {len(analysis['endpoints'])} endpoints",
|
|
446
|
+
data=analysis
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Code generation helpers
|
|
450
|
+
|
|
451
|
+
def _generate_main(self, name: str, with_db: bool, with_auth: bool) -> str:
|
|
452
|
+
"""Generate main.py content"""
|
|
453
|
+
imports = ['from fastapi import FastAPI', 'from fastapi.middleware.cors import CORSMiddleware']
|
|
454
|
+
|
|
455
|
+
if with_db:
|
|
456
|
+
imports.append('from app.db.database import engine, Base')
|
|
457
|
+
|
|
458
|
+
code = '\n'.join(imports) + '\n'
|
|
459
|
+
code += f'''from app.api.v1 import health
|
|
460
|
+
|
|
461
|
+
app = FastAPI(
|
|
462
|
+
title="{name.replace('_', ' ').title()} API",
|
|
463
|
+
description="API built with FastAPI",
|
|
464
|
+
version="1.0.0"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# CORS middleware
|
|
468
|
+
app.add_middleware(
|
|
469
|
+
CORSMiddleware,
|
|
470
|
+
allow_origins=["*"],
|
|
471
|
+
allow_credentials=True,
|
|
472
|
+
allow_methods=["*"],
|
|
473
|
+
allow_headers=["*"],
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Include routers
|
|
477
|
+
app.include_router(health.router, prefix="/api/v1")
|
|
478
|
+
'''
|
|
479
|
+
|
|
480
|
+
if with_db:
|
|
481
|
+
code += '''
|
|
482
|
+
|
|
483
|
+
# Create database tables
|
|
484
|
+
@app.on_event("startup")
|
|
485
|
+
async def startup():
|
|
486
|
+
Base.metadata.create_all(bind=engine)
|
|
487
|
+
'''
|
|
488
|
+
|
|
489
|
+
code += '''
|
|
490
|
+
|
|
491
|
+
@app.get("/")
|
|
492
|
+
async def root():
|
|
493
|
+
return {"message": "Welcome to the API", "docs": "/docs"}
|
|
494
|
+
'''
|
|
495
|
+
return code
|
|
496
|
+
|
|
497
|
+
def _generate_config(self, name: str, with_db: bool) -> str:
|
|
498
|
+
"""Generate config.py content"""
|
|
499
|
+
code = '''"""
|
|
500
|
+
Application configuration
|
|
501
|
+
"""
|
|
502
|
+
from pydantic_settings import BaseSettings
|
|
503
|
+
from functools import lru_cache
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class Settings(BaseSettings):
|
|
507
|
+
"""Application settings"""
|
|
508
|
+
app_name: str = "''' + name + '''"
|
|
509
|
+
debug: bool = False
|
|
510
|
+
api_v1_prefix: str = "/api/v1"
|
|
511
|
+
'''
|
|
512
|
+
if with_db:
|
|
513
|
+
code += ''' database_url: str = "sqlite:///./app.db"
|
|
514
|
+
'''
|
|
515
|
+
code += '''
|
|
516
|
+
class Config:
|
|
517
|
+
env_file = ".env"
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@lru_cache()
|
|
521
|
+
def get_settings() -> Settings:
|
|
522
|
+
return Settings()
|
|
523
|
+
'''
|
|
524
|
+
return code
|
|
525
|
+
|
|
526
|
+
def _generate_requirements(self, with_db: bool, with_auth: bool) -> str:
|
|
527
|
+
"""Generate requirements.txt"""
|
|
528
|
+
reqs = [
|
|
529
|
+
"fastapi>=0.100.0",
|
|
530
|
+
"uvicorn[standard]>=0.23.0",
|
|
531
|
+
"pydantic>=2.0.0",
|
|
532
|
+
"pydantic-settings>=2.0.0",
|
|
533
|
+
"python-dotenv>=1.0.0",
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
if with_db:
|
|
537
|
+
reqs.extend([
|
|
538
|
+
"sqlalchemy>=2.0.0",
|
|
539
|
+
"alembic>=1.12.0",
|
|
540
|
+
])
|
|
541
|
+
|
|
542
|
+
if with_auth:
|
|
543
|
+
reqs.extend([
|
|
544
|
+
"python-jose[cryptography]>=3.3.0",
|
|
545
|
+
"passlib[bcrypt]>=1.7.0",
|
|
546
|
+
"python-multipart>=0.0.6",
|
|
547
|
+
])
|
|
548
|
+
|
|
549
|
+
reqs.append("pytest>=7.0.0")
|
|
550
|
+
reqs.append("httpx>=0.24.0")
|
|
551
|
+
|
|
552
|
+
return '\n'.join(reqs) + '\n'
|
|
553
|
+
|
|
554
|
+
def _generate_env_example(self, name: str, with_db: bool) -> str:
|
|
555
|
+
"""Generate .env.example"""
|
|
556
|
+
content = f'''# {name} Environment Variables
|
|
557
|
+
DEBUG=true
|
|
558
|
+
'''
|
|
559
|
+
if with_db:
|
|
560
|
+
content += 'DATABASE_URL=sqlite:///./app.db\n'
|
|
561
|
+
return content
|
|
562
|
+
|
|
563
|
+
def _generate_example_router(self) -> str:
|
|
564
|
+
"""Generate example health router"""
|
|
565
|
+
return '''"""
|
|
566
|
+
Health check router
|
|
567
|
+
"""
|
|
568
|
+
from fastapi import APIRouter
|
|
569
|
+
|
|
570
|
+
router = APIRouter(prefix="/health", tags=["health"])
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@router.get("/")
|
|
574
|
+
async def health_check():
|
|
575
|
+
"""Health check endpoint"""
|
|
576
|
+
return {"status": "healthy"}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@router.get("/ready")
|
|
580
|
+
async def readiness_check():
|
|
581
|
+
"""Readiness check endpoint"""
|
|
582
|
+
return {"status": "ready"}
|
|
583
|
+
'''
|
|
584
|
+
|
|
585
|
+
def _generate_api_init(self) -> str:
|
|
586
|
+
"""Generate API __init__.py"""
|
|
587
|
+
return '''"""
|
|
588
|
+
API v1 routers
|
|
589
|
+
"""
|
|
590
|
+
from . import health
|
|
591
|
+
|
|
592
|
+
__all__ = ["health"]
|
|
593
|
+
'''
|
|
594
|
+
|
|
595
|
+
def _generate_db_setup(self) -> str:
|
|
596
|
+
"""Generate database setup"""
|
|
597
|
+
return '''"""
|
|
598
|
+
Database configuration
|
|
599
|
+
"""
|
|
600
|
+
from sqlalchemy import create_engine
|
|
601
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
602
|
+
from app.core.config import get_settings
|
|
603
|
+
|
|
604
|
+
settings = get_settings()
|
|
605
|
+
|
|
606
|
+
engine = create_engine(
|
|
607
|
+
settings.database_url,
|
|
608
|
+
connect_args={"check_same_thread": False} # SQLite only
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
612
|
+
|
|
613
|
+
Base = declarative_base()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def get_db():
|
|
617
|
+
"""Dependency for database session"""
|
|
618
|
+
db = SessionLocal()
|
|
619
|
+
try:
|
|
620
|
+
yield db
|
|
621
|
+
finally:
|
|
622
|
+
db.close()
|
|
623
|
+
'''
|
|
624
|
+
|
|
625
|
+
def _generate_base_model(self) -> str:
|
|
626
|
+
"""Generate base SQLAlchemy model"""
|
|
627
|
+
return '''"""
|
|
628
|
+
Base model with common fields
|
|
629
|
+
"""
|
|
630
|
+
from datetime import datetime
|
|
631
|
+
from sqlalchemy import Column, Integer, DateTime
|
|
632
|
+
from app.db.database import Base
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class BaseModel(Base):
|
|
636
|
+
"""Abstract base model with common fields"""
|
|
637
|
+
__abstract__ = True
|
|
638
|
+
|
|
639
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
640
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
641
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
642
|
+
'''
|
|
643
|
+
|
|
644
|
+
def _generate_auth_module(self) -> str:
|
|
645
|
+
"""Generate authentication module"""
|
|
646
|
+
return '''"""
|
|
647
|
+
Authentication utilities
|
|
648
|
+
"""
|
|
649
|
+
from datetime import datetime, timedelta
|
|
650
|
+
from typing import Optional
|
|
651
|
+
from jose import JWTError, jwt
|
|
652
|
+
from passlib.context import CryptContext
|
|
653
|
+
from fastapi import Depends, HTTPException, status
|
|
654
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
655
|
+
|
|
656
|
+
# Configuration
|
|
657
|
+
SECRET_KEY = "your-secret-key-change-in-production"
|
|
658
|
+
ALGORITHM = "HS256"
|
|
659
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
660
|
+
|
|
661
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
662
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
666
|
+
"""Verify a password against its hash"""
|
|
667
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def get_password_hash(password: str) -> str:
|
|
671
|
+
"""Hash a password"""
|
|
672
|
+
return pwd_context.hash(password)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
|
676
|
+
"""Create a JWT access token"""
|
|
677
|
+
to_encode = data.copy()
|
|
678
|
+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
|
|
679
|
+
to_encode.update({"exp": expire})
|
|
680
|
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
684
|
+
"""Get current user from JWT token"""
|
|
685
|
+
credentials_exception = HTTPException(
|
|
686
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
687
|
+
detail="Could not validate credentials",
|
|
688
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
689
|
+
)
|
|
690
|
+
try:
|
|
691
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
692
|
+
username: str = payload.get("sub")
|
|
693
|
+
if username is None:
|
|
694
|
+
raise credentials_exception
|
|
695
|
+
except JWTError:
|
|
696
|
+
raise credentials_exception
|
|
697
|
+
return {"username": username}
|
|
698
|
+
'''
|
|
699
|
+
|
|
700
|
+
def _path_to_function_name(self, path: str, method: str) -> str:
|
|
701
|
+
"""Convert URL path to function name"""
|
|
702
|
+
# Remove path parameters
|
|
703
|
+
clean_path = re.sub(r'\{[^}]+\}', '', path)
|
|
704
|
+
# Convert to snake_case
|
|
705
|
+
name = clean_path.strip('/').replace('/', '_').replace('-', '_')
|
|
706
|
+
prefix = method.lower()
|
|
707
|
+
return f"{prefix}_{name}" if name else prefix
|
|
708
|
+
|
|
709
|
+
def _generate_endpoint_code(
|
|
710
|
+
self,
|
|
711
|
+
path: str,
|
|
712
|
+
method: str,
|
|
713
|
+
name: str,
|
|
714
|
+
path_params: List[str]
|
|
715
|
+
) -> str:
|
|
716
|
+
"""Generate endpoint code"""
|
|
717
|
+
decorator = f'@router.{method.lower()}("{path}")'
|
|
718
|
+
|
|
719
|
+
# Build function signature
|
|
720
|
+
params = [f"{p}: int" for p in path_params] # Default to int
|
|
721
|
+
params_str = ", ".join(params) if params else ""
|
|
722
|
+
|
|
723
|
+
if method in ["POST", "PUT", "PATCH"]:
|
|
724
|
+
# Add request body parameter
|
|
725
|
+
if params_str:
|
|
726
|
+
params_str += ", "
|
|
727
|
+
params_str += "data: dict"
|
|
728
|
+
|
|
729
|
+
code = f'''{decorator}
|
|
730
|
+
async def {name}({params_str}):
|
|
731
|
+
"""
|
|
732
|
+
{method} {path}
|
|
733
|
+
"""
|
|
734
|
+
'''
|
|
735
|
+
if method == "GET":
|
|
736
|
+
if path_params:
|
|
737
|
+
code += f' return {{"id": {path_params[0]}}}\n'
|
|
738
|
+
else:
|
|
739
|
+
code += ' return {"message": "OK"}\n'
|
|
740
|
+
elif method == "POST":
|
|
741
|
+
code += ' return {"message": "Created", "data": data}\n'
|
|
742
|
+
elif method in ["PUT", "PATCH"]:
|
|
743
|
+
code += f' return {{"id": {path_params[0] if path_params else "1"}, "updated": True}}\n'
|
|
744
|
+
elif method == "DELETE":
|
|
745
|
+
code += ' return None\n'
|
|
746
|
+
|
|
747
|
+
return code
|
|
748
|
+
|
|
749
|
+
def _generate_pydantic_model(self, name: str, fields: Dict[str, str]) -> str:
|
|
750
|
+
"""Generate Pydantic model code"""
|
|
751
|
+
code = f'''from pydantic import BaseModel
|
|
752
|
+
from typing import Optional
|
|
753
|
+
from datetime import datetime
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
class {name}Base(BaseModel):
|
|
757
|
+
"""Base {name} schema"""
|
|
758
|
+
'''
|
|
759
|
+
for field_name, field_type in fields.items():
|
|
760
|
+
code += f' {field_name}: {field_type}\n'
|
|
761
|
+
|
|
762
|
+
code += f'''
|
|
763
|
+
|
|
764
|
+
class {name}Create({name}Base):
|
|
765
|
+
"""Schema for creating {name}"""
|
|
766
|
+
pass
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class {name}Update(BaseModel):
|
|
770
|
+
"""Schema for updating {name}"""
|
|
771
|
+
'''
|
|
772
|
+
for field_name, field_type in fields.items():
|
|
773
|
+
code += f' {field_name}: Optional[{field_type}] = None\n'
|
|
774
|
+
|
|
775
|
+
code += f'''
|
|
776
|
+
|
|
777
|
+
class {name}({name}Base):
|
|
778
|
+
"""Full {name} schema with ID"""
|
|
779
|
+
id: int
|
|
780
|
+
created_at: datetime
|
|
781
|
+
updated_at: datetime
|
|
782
|
+
|
|
783
|
+
class Config:
|
|
784
|
+
from_attributes = True
|
|
785
|
+
'''
|
|
786
|
+
return code
|
|
787
|
+
|
|
788
|
+
def _generate_crud_router(self, model: str, prefix: str) -> str:
|
|
789
|
+
"""Generate CRUD router code"""
|
|
790
|
+
model_lower = model.lower()
|
|
791
|
+
|
|
792
|
+
return f'''"""
|
|
793
|
+
{model} CRUD Router
|
|
794
|
+
"""
|
|
795
|
+
from typing import List
|
|
796
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
797
|
+
from sqlalchemy.orm import Session
|
|
798
|
+
from app.db.database import get_db
|
|
799
|
+
# from app.models.{model_lower} import {model}
|
|
800
|
+
# from app.schemas.{model_lower} import {model}Create, {model}Update, {model} as {model}Schema
|
|
801
|
+
|
|
802
|
+
router = APIRouter(prefix="{prefix}", tags=["{model_lower}s"])
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
@router.get("/")
|
|
806
|
+
async def list_{model_lower}s(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
|
807
|
+
"""List all {model_lower}s"""
|
|
808
|
+
# items = db.query({model}).offset(skip).limit(limit).all()
|
|
809
|
+
return {{"items": [], "total": 0}}
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@router.post("/", status_code=status.HTTP_201_CREATED)
|
|
813
|
+
async def create_{model_lower}(data: dict, db: Session = Depends(get_db)):
|
|
814
|
+
"""Create a new {model_lower}"""
|
|
815
|
+
# db_item = {model}(**data)
|
|
816
|
+
# db.add(db_item)
|
|
817
|
+
# db.commit()
|
|
818
|
+
# db.refresh(db_item)
|
|
819
|
+
return {{"message": "Created", "data": data}}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@router.get("/{{item_id}}")
|
|
823
|
+
async def get_{model_lower}(item_id: int, db: Session = Depends(get_db)):
|
|
824
|
+
"""Get a {model_lower} by ID"""
|
|
825
|
+
# item = db.query({model}).filter({model}.id == item_id).first()
|
|
826
|
+
# if not item:
|
|
827
|
+
# raise HTTPException(status_code=404, detail="{model} not found")
|
|
828
|
+
return {{"id": item_id}}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@router.put("/{{item_id}}")
|
|
832
|
+
async def update_{model_lower}(item_id: int, data: dict, db: Session = Depends(get_db)):
|
|
833
|
+
"""Update a {model_lower}"""
|
|
834
|
+
# item = db.query({model}).filter({model}.id == item_id).first()
|
|
835
|
+
# if not item:
|
|
836
|
+
# raise HTTPException(status_code=404, detail="{model} not found")
|
|
837
|
+
return {{"id": item_id, "updated": True}}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@router.delete("/{{item_id}}", status_code=status.HTTP_204_NO_CONTENT)
|
|
841
|
+
async def delete_{model_lower}(item_id: int, db: Session = Depends(get_db)):
|
|
842
|
+
"""Delete a {model_lower}"""
|
|
843
|
+
# item = db.query({model}).filter({model}.id == item_id).first()
|
|
844
|
+
# if not item:
|
|
845
|
+
# raise HTTPException(status_code=404, detail="{model} not found")
|
|
846
|
+
# db.delete(item)
|
|
847
|
+
# db.commit()
|
|
848
|
+
return None
|
|
849
|
+
'''
|
|
850
|
+
|
|
851
|
+
def can_handle(self, request: str) -> float:
|
|
852
|
+
"""Check if request is FastAPI-related"""
|
|
853
|
+
request_lower = request.lower()
|
|
854
|
+
|
|
855
|
+
# High confidence
|
|
856
|
+
high_conf = ["fastapi", "pydantic", "uvicorn", "api endpoint"]
|
|
857
|
+
for kw in high_conf:
|
|
858
|
+
if kw in request_lower:
|
|
859
|
+
return 0.9
|
|
860
|
+
|
|
861
|
+
# Medium confidence
|
|
862
|
+
med_conf = ["rest api", "endpoint", "router", "crud", "schema"]
|
|
863
|
+
for kw in med_conf:
|
|
864
|
+
if kw in request_lower:
|
|
865
|
+
return 0.6
|
|
866
|
+
|
|
867
|
+
return super().can_handle(request)
|
|
868
|
+
|
|
869
|
+
def handle_request(self, request: str, **kwargs) -> Optional[ActionResult]:
|
|
870
|
+
"""Handle a natural language request"""
|
|
871
|
+
request_lower = request.lower()
|
|
872
|
+
|
|
873
|
+
if "scaffold" in request_lower or "new project" in request_lower:
|
|
874
|
+
# Extract project name
|
|
875
|
+
words = request.split()
|
|
876
|
+
name = "my_api"
|
|
877
|
+
for i, word in enumerate(words):
|
|
878
|
+
if word.lower() in ["project", "called", "named"]:
|
|
879
|
+
if i + 1 < len(words):
|
|
880
|
+
name = words[i + 1].strip("'\"")
|
|
881
|
+
break
|
|
882
|
+
return self.scaffold_project(name=name)
|
|
883
|
+
|
|
884
|
+
if "analyze" in request_lower:
|
|
885
|
+
return self.analyze_project()
|
|
886
|
+
|
|
887
|
+
return None
|