alms-cli 0.1.0__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.
@@ -0,0 +1,1329 @@
1
+ """Project template generator."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import shutil
6
+
7
+ class TemplateGenerator:
8
+ """Generate ALMS project structure from templates."""
9
+
10
+ def __init__(self, project_name: str, project_path: Path):
11
+ self.project_name = project_name
12
+ self.project_path = project_path
13
+ self.template_dir = Path(__file__).parent / "base"
14
+
15
+ def generate(self, features: Optional[list[str]] = None) -> int:
16
+ """Generate project structure and return file count."""
17
+ if features is None:
18
+ features = []
19
+
20
+ self._create_base_structure()
21
+ self._create_config_files()
22
+ self._create_source_files()
23
+
24
+ return self._count_files()
25
+
26
+ def _create_base_structure(self):
27
+ """Create base directory structure."""
28
+ dirs = [
29
+ "src/api/endpoints/v1/schemas",
30
+ "src/api/middlewares",
31
+ "src/api/router",
32
+ "src/execution/actions",
33
+ "src/execution/usecases",
34
+ "src/agents/agent_manager",
35
+ "src/agents/prompts",
36
+ "src/agents/tools",
37
+ "src/agents/workflows",
38
+ "src/providers/ai",
39
+ "src/providers/cache",
40
+ "src/providers/vectordb",
41
+ "src/database/migrations",
42
+ "src/database/repositories",
43
+ "src/core",
44
+ "src/config",
45
+ "src/models",
46
+ "src/utils",
47
+ "src/scripts",
48
+ "src/docs",
49
+ "src/evaluation",
50
+ "src/data",
51
+ "src/tests/v1",
52
+ "src/tests/integration",
53
+ "src/tests/e2e",
54
+ "alembic",
55
+ "rules",
56
+ "docs/changelogs",
57
+ ".github/workflows",
58
+ ".github/ISSUE_TEMPLATE",
59
+ "assets/images",
60
+ ]
61
+
62
+ for dir_path in dirs:
63
+ (self.project_path / dir_path).mkdir(parents=True, exist_ok=True)
64
+
65
+ def _create_config_files(self):
66
+ """Create configuration files."""
67
+ templates = {
68
+ "pyproject.toml": self._pyproject_toml(),
69
+ "README.md": self._readme(),
70
+ ".env.example": self._env_example(),
71
+ ".gitignore": self._gitignore(),
72
+ "pytest.ini": self._pytest_ini(),
73
+ "alembic.ini": self._alembic_ini(),
74
+ "docker-compose.yml": self._docker_compose(),
75
+ "Dockerfile": self._dockerfile(),
76
+ "rules/project_rules.md": self._project_rules(),
77
+ "docs/01-System-Design.md": "# System Design\n",
78
+ "docs/02-Design-Patterns.md": "# Design Patterns\n",
79
+ "docs/03-Database-Design.md": "# Database Design\n",
80
+ "docs/04-Tech-Stack.md": "# Tech Stack\n",
81
+ "docs/05-Project-Structure.md": "# Project Structure\n",
82
+ "docs/06-API-Documentation.md": "# API Documentation\n",
83
+ "docs/07-Setup-Installation.md": "# Setup & Installation\n",
84
+ "docs/08-Contribution-Guide.md": "# Contribution Guide\n",
85
+ ".github/workflows/ci.yml": self._ci_workflow(),
86
+ ".github/dependabot.yml": self._dependabot(),
87
+ ".github/ISSUE_TEMPLATE/bug_report.yml": self._bug_template(),
88
+ ".github/ISSUE_TEMPLATE/feature_request.yml": self._feature_template(),
89
+ ".github/pull_request_template.md": self._pr_template(),
90
+ "alembic/README": "Alembic migrations\n",
91
+ "alembic/env.py": self._alembic_env(),
92
+ "alembic/script.py.mako": self._alembic_mako(),
93
+ }
94
+
95
+ for file_path, content in templates.items():
96
+ full_path = self.project_path / file_path
97
+ full_path.parent.mkdir(parents=True, exist_ok=True)
98
+ full_path.write_text(content)
99
+
100
+ def _create_source_files(self):
101
+ """Create source code files."""
102
+ templates = {
103
+ "src/__init__.py": "",
104
+ "src/api/__init__.py": "",
105
+ "src/api/main.py": self._main_py(),
106
+ "src/api/endpoints/__init__.py": "",
107
+ "src/api/endpoints/v1/__init__.py": "",
108
+ "src/api/endpoints/v1/dependencies.py": self._dependencies_py(),
109
+ "src/api/endpoints/v1/health.py": self._health_py(),
110
+ "src/api/endpoints/v1/metrics.py": self._metrics_py(),
111
+ "src/api/endpoints/v1/routers.py": self._routers_py(),
112
+ "src/api/endpoints/v1/sample_agent.py": self._sample_agent_py(),
113
+ "src/api/endpoints/v1/sample_di.py": self._sample_di_py(),
114
+ "src/api/endpoints/v1/schemas/__init__.py": "",
115
+ "src/api/endpoints/v1/schemas/base.py": self._schema_base_py(),
116
+ "src/api/endpoints/v1/schemas/sample.py": self._schema_sample_py(),
117
+ "src/api/middlewares/__init__.py": "",
118
+ "src/api/middlewares/error_handler.py": self._error_handler_py(),
119
+ "src/api/middlewares/logging.py": self._logging_middleware_py(),
120
+ "src/api/middlewares/observability.py": self._observability_middleware_py(),
121
+ "src/api/middlewares/security.py": self._security_middleware_py(),
122
+ "src/api/router/__init__.py": "",
123
+ "src/api/router/routers.py": self._router_py(),
124
+ "src/execution/__init__.py": "",
125
+ "src/execution/actions/__init__.py": "",
126
+ "src/execution/actions/sample_action.py": self._sample_action_py(),
127
+ "src/execution/usecases/__init__.py": "",
128
+ "src/execution/usecases/sample_usecase.py": self._sample_usecase_py(),
129
+ "src/agents/__init__.py": "",
130
+ "src/agents/agent_manager/__init__.py": "",
131
+ "src/agents/agent_manager/agent.py": self._agent_py(),
132
+ "src/agents/prompts/__init__.py": "",
133
+ "src/agents/prompts/sample_agent_prompt.py": self._sample_prompt_py(),
134
+ "src/agents/tools/.gitkeep": "",
135
+ "src/agents/workflows/.gitkeep": "",
136
+ "src/providers/__init__.py": "",
137
+ "src/providers/ai/__init__.py": "",
138
+ "src/providers/ai/langchain_model_loader.py": self._model_loader_py(),
139
+ "src/providers/cache/.gitkeep": "",
140
+ "src/providers/vectordb/.gitkeep": "",
141
+ "src/observability/__init__.py": self._observability_init_py(),
142
+ "src/observability/metrics.py": self._metrics_module_py(),
143
+ "src/observability/tracing.py": self._tracing_py(),
144
+ "src/database/__init__.py": "",
145
+ "src/database/connection.py": self._db_connection_py(),
146
+ "src/database/migrations/.gitkeep": "",
147
+ "src/database/repositories/__init__.py": "",
148
+ "src/database/repositories/base.py": self._repository_base_py(),
149
+ "src/database/repositories/sqlalchemy_repository.py": self._sqlalchemy_repo_py(),
150
+ "src/core/__init__.py": "",
151
+ "src/core/exceptions.py": self._exceptions_py(),
152
+ "src/config/__init__.py": "",
153
+ "src/config/settings.py": self._settings_py(),
154
+ "src/config/logs_config.py": self._logs_config_py(),
155
+ "src/models/.gitkeep": "",
156
+ "src/utils/.gitkeep": "",
157
+ "src/scripts/.gitkeep": "",
158
+ "src/docs/.gitkeep": "",
159
+ "src/evaluation/.gitkeep": "",
160
+ "src/data/.gitkeep": "",
161
+ "src/tests/__init__.py": "",
162
+ "src/tests/conftest.py": self._conftest_py(),
163
+ "src/tests/README.md": "# Tests\n",
164
+ "src/tests/v1/__init__.py": "",
165
+ "src/tests/v1/test_health.py": self._test_health_py(),
166
+ "src/tests/v1/test_agent.py": self._test_agent_py(),
167
+ "src/tests/v1/test_metrics.py": self._test_metrics_py(),
168
+ "src/tests/v1/test_metrics_endpoint.py": self._test_metrics_endpoint_py(),
169
+ "src/tests/v1/test_observability_middleware.py": self._test_observability_middleware_py(),
170
+ "src/tests/v1/test_sqlalchemy_repository.py": self._test_sqlalchemy_repository_py(),
171
+ "src/tests/v1/test_tracing.py": self._test_tracing_py(),
172
+ "src/tests/integration/__init__.py": "",
173
+ "src/tests/integration/test_full_stack.py": self._test_full_stack_py(),
174
+ "src/tests/e2e/__init__.py": "",
175
+ "src/tests/e2e/test_workflows.py": self._test_workflows_py(),
176
+ }
177
+
178
+ for file_path, content in templates.items():
179
+ full_path = self.project_path / file_path
180
+ full_path.parent.mkdir(parents=True, exist_ok=True)
181
+ full_path.write_text(content)
182
+
183
+ def _count_files(self) -> int:
184
+ """Count created files."""
185
+ count = 0
186
+ for path in self.project_path.rglob("*"):
187
+ if path.is_file() and not path.name.startswith('.'):
188
+ count += 1
189
+ return count
190
+
191
+ def _pyproject_toml(self) -> str:
192
+ return f'''[project]
193
+ name = "{self.project_name}"
194
+ version = "0.1.0"
195
+ description = "ALMS AI-first backend starter"
196
+ readme = "README.md"
197
+ requires-python = ">=3.13"
198
+ dependencies = [
199
+ "alembic>=1.17.2",
200
+ "asyncpg>=0.31.0",
201
+ "fastapi[standard]>=0.122.0",
202
+ "langchain>=1.1.0",
203
+ "langchain-community>=0.4.1",
204
+ "langchain-mcp-adapters>=0.1.14",
205
+ "langchain-openai>=1.1.0",
206
+ "langgraph>=1.0.4",
207
+ "openai>=2.8.1",
208
+ "opentelemetry-api>=1.25.0",
209
+ "opentelemetry-sdk>=1.25.0",
210
+ "opentelemetry-instrumentation-fastapi>=0.46b0",
211
+ "opentelemetry-instrumentation-sqlalchemy>=0.46b0",
212
+ "opentelemetry-instrumentation-redis>=0.46b0",
213
+ "opentelemetry-exporter-otlp>=1.25.0",
214
+ "prometheus-client>=0.20.0",
215
+ "pydantic>=2.12.5",
216
+ "pydantic-settings>=2.12.0",
217
+ "redis>=7.1.0",
218
+ "ruff>=0.14.11",
219
+ "scalar-fastapi>=1.6.0",
220
+ "sqlalchemy>=2.0.44",
221
+ "uvicorn>=0.38.0",
222
+ ]
223
+
224
+ [dependency-groups]
225
+ dev = [
226
+ "httpx>=0.28.1",
227
+ "pytest>=9.0.2",
228
+ "pytest-asyncio>=1.3.0",
229
+ ]
230
+ '''
231
+
232
+ def _readme(self) -> str:
233
+ return f'''# {self.project_name}
234
+
235
+ > **The AI-First Backend for Scalable, Intelligent Applications.**
236
+
237
+ ## Quick Start
238
+
239
+ ### Prerequisites
240
+
241
+ - Python 3.13+
242
+ - PostgreSQL (optional)
243
+ - Redis (optional)
244
+ - [uv](https://docs.astral.sh/uv/getting-started/installation/) installed
245
+
246
+ ### 1. Setup
247
+
248
+ ```bash
249
+ # Copy environment file
250
+ cp .env.example .env
251
+
252
+ # Edit .env with your credentials
253
+ ```
254
+
255
+ ### 2. Install Dependencies
256
+
257
+ ```bash
258
+ uv sync
259
+ ```
260
+
261
+ ### 3. Run Database Migrations
262
+
263
+ ```bash
264
+ alembic revision --autogenerate -m "Initial migration"
265
+ alembic upgrade head
266
+ ```
267
+
268
+ ### 4. Start the Application
269
+
270
+ ```bash
271
+ uv run -m src.api.main
272
+ ```
273
+
274
+ The API will be available at:
275
+ - API: http://localhost:3000
276
+ - Docs: http://localhost:3000/docs
277
+ '''
278
+
279
+ def _env_example(self) -> str:
280
+ return '''# OpenAI Configuration
281
+ OPENAI_API_KEY=sk-...
282
+ OPENAI_MODEL_BASIC=gpt-4o-mini
283
+ OPENAI_MODEL_REASONING=gpt-4o
284
+
285
+ # Database Configuration
286
+ DATABASE_HOST=localhost
287
+ DATABASE_PORT=5432
288
+ DATABASE_NAME=mydb
289
+ DATABASE_USER=postgres
290
+ DATABASE_PASSWORD=secret
291
+
292
+ # Redis Configuration
293
+ REDIS_HOST=localhost
294
+ REDIS_PORT=6379
295
+
296
+ # Observability (Optional)
297
+ OTLP_ENDPOINT=http://localhost:4317
298
+ METRICS_ENABLED=true
299
+ TRACING_ENABLED=true
300
+
301
+ # Application
302
+ DEBUG=true
303
+ LOG_LEVEL=info
304
+ SERVER_PORT=3000
305
+ '''
306
+
307
+ def _gitignore(self) -> str:
308
+ return '''# Byte-compiled / optimized / DLL files
309
+ __pycache__/
310
+ *.py[cod]
311
+ *$py.class
312
+
313
+ # Virtual environments
314
+ .venv/
315
+ venv/
316
+
317
+ # Distribution / packaging
318
+ dist/
319
+ build/
320
+ *.egg-info/
321
+
322
+ # IDE
323
+ .vscode/
324
+ .idea/
325
+ *.swp
326
+ *.swo
327
+
328
+ # Environment
329
+ .env
330
+ .env.local
331
+
332
+ # Database
333
+ *.db
334
+ *.sqlite3
335
+
336
+ # Logs
337
+ *.log
338
+ logs/
339
+
340
+ # OS
341
+ .DS_Store
342
+ Thumbs.db
343
+
344
+ # Testing
345
+ .pytest_cache/
346
+ .coverage
347
+ htmlcov/
348
+
349
+ # Alembic
350
+ alembic/versions/*.pyc
351
+ '''
352
+
353
+ def _pytest_ini(self) -> str:
354
+ return '''[pytest]
355
+ testpaths = src/tests
356
+ python_files = test_*.py
357
+ python_classes = Test*
358
+ python_functions = test_*
359
+ asyncio_mode = auto
360
+ '''
361
+
362
+ def _alembic_ini(self) -> str:
363
+ return '''[alembic]
364
+ script_location = alembic
365
+ sqlalchemy.url = postgresql+asyncpg://postgres:secret@localhost:5432/mydb
366
+
367
+ [loggers]
368
+ keys = root,sqlalchemy,alembic
369
+
370
+ [handlers]
371
+ keys = console
372
+
373
+ [formatters]
374
+ keys = generic
375
+
376
+ [logger_root]
377
+ level = WARN
378
+ handlers = console
379
+
380
+ [logger_sqlalchemy]
381
+ level = WARN
382
+ handlers =
383
+ qualname = sqlalchemy.engine
384
+
385
+ [logger_alembic]
386
+ level = INFO
387
+ handlers =
388
+ qualname = alembic
389
+
390
+ [handler_console]
391
+ class = StreamHandler
392
+ args = (sys.stderr,)
393
+ level = NOTSET
394
+ formatter = generic
395
+
396
+ [formatter_generic]
397
+ format = %(levelname)-5.5s [%(name)s] %(message)s
398
+ datefmt = %H:%M:%S
399
+ '''
400
+
401
+ def _docker_compose(self) -> str:
402
+ return '''version: '3.8'
403
+
404
+ services:
405
+ postgres:
406
+ image: postgres:16-alpine
407
+ environment:
408
+ POSTGRES_DB: mydb
409
+ POSTGRES_USER: postgres
410
+ POSTGRES_PASSWORD: secret
411
+ ports:
412
+ - "5432:5432"
413
+ volumes:
414
+ - postgres_data:/var/lib/postgresql/data
415
+
416
+ redis:
417
+ image: redis:7-alpine
418
+ ports:
419
+ - "6379:6379"
420
+ volumes:
421
+ - redis_data:/data
422
+
423
+ volumes:
424
+ postgres_data:
425
+ redis_data:
426
+ '''
427
+
428
+ def _dockerfile(self) -> str:
429
+ return '''FROM python:3.13-slim
430
+
431
+ WORKDIR /app
432
+
433
+ RUN pip install uv
434
+
435
+ COPY pyproject.toml uv.lock ./
436
+ RUN uv sync --frozen --no-dev
437
+
438
+ COPY . .
439
+
440
+ EXPOSE 3000
441
+
442
+ CMD ["uv", "run", "-m", "src.api.main", "--host", "0.0.0.0"]
443
+ '''
444
+
445
+ def _project_rules(self) -> str:
446
+ return '''# Project Rules
447
+
448
+ ## ALMS Architecture Guidelines
449
+
450
+ 1. Never skip layers: API -> Execution -> Agent -> Infrastructure
451
+ 2. Use repository pattern for database access
452
+ 3. Use custom exceptions, not raw HTTPException
453
+ 4. Add type hints to all functions
454
+ 5. Write tests for all use cases and actions
455
+ '''
456
+
457
+ def _ci_workflow(self) -> str:
458
+ return '''name: CI
459
+
460
+ on:
461
+ push:
462
+ branches: [main]
463
+ pull_request:
464
+ branches: [main]
465
+
466
+ jobs:
467
+ test:
468
+ runs-on: ubuntu-latest
469
+ steps:
470
+ - uses: actions/checkout@v4
471
+ - uses: astral-sh/setup-uv@v3
472
+ - run: uv sync
473
+ - run: uv run pytest src/tests
474
+ - run: uv run ruff check src/
475
+ '''
476
+
477
+ def _dependabot(self) -> str:
478
+ return '''version: 2
479
+ updates:
480
+ - package-ecosystem: "pip"
481
+ directory: "/"
482
+ schedule:
483
+ interval: "weekly"
484
+ '''
485
+
486
+ def _bug_template(self) -> str:
487
+ return '''name: Bug Report
488
+ description: Create a report to help us improve
489
+ body:
490
+ - type: textarea
491
+ attributes:
492
+ label: Describe the bug
493
+ validations:
494
+ required: true
495
+ '''
496
+
497
+ def _feature_template(self) -> str:
498
+ return '''name: Feature Request
499
+ description: Suggest an idea for this project
500
+ body:
501
+ - type: textarea
502
+ attributes:
503
+ label: Describe the feature
504
+ validations:
505
+ required: true
506
+ '''
507
+
508
+ def _pr_template(self) -> str:
509
+ return '''## Description
510
+
511
+ Please include a summary of the changes.
512
+
513
+ ## Type of change
514
+
515
+ - [ ] Bug fix
516
+ - [ ] New feature
517
+ - [ ] Breaking change
518
+ '''
519
+
520
+ def _alembic_env(self) -> str:
521
+ return '''from logging.config import fileConfig
522
+ from sqlalchemy import engine_from_config
523
+ from sqlalchemy import pool
524
+ from alembic import context
525
+
526
+ config = context.config
527
+
528
+ if config.config_file_name is not None:
529
+ fileConfig(config.config_file_name)
530
+
531
+ target_metadata = None
532
+
533
+ def run_migrations_offline() -> None:
534
+ url = config.get_main_option("sqlalchemy.url")
535
+ context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
536
+ with context.begin_transaction():
537
+ context.run_migrations()
538
+
539
+ def run_migrations_online() -> None:
540
+ connectable = engine_from_config(
541
+ config.get_section(config.config_ini_section, {}),
542
+ prefix="sqlalchemy.",
543
+ poolclass=pool.NullPool,
544
+ )
545
+ with connectable.connect() as connection:
546
+ context.configure(connection=connection, target_metadata=target_metadata)
547
+ with context.begin_transaction():
548
+ context.run_migrations()
549
+
550
+ if context.is_offline_mode():
551
+ run_migrations_offline()
552
+ else:
553
+ run_migrations_online()
554
+ '''
555
+
556
+ def _alembic_mako(self) -> str:
557
+ return '''"""${message}
558
+
559
+ Revision ID: ${up_revision}
560
+ Revises: ${down_revision | comma,n}
561
+ Create Date: ${create_date}
562
+ """
563
+ from typing import Sequence, Union
564
+ from alembic import op
565
+ import sqlalchemy as sa
566
+
567
+ revision: str = ${repr(up_revision)}
568
+ down_revision: Union[str, None] = ${repr(down_revision)}
569
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
570
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
571
+
572
+ def upgrade() -> None:
573
+ ${upgrades if upgrades else "pass"}
574
+
575
+ def downgrade() -> None:
576
+ ${downgrades if downgrades else "pass"}
577
+ '''
578
+
579
+ def _main_py(self) -> str:
580
+ return '''"""FastAPI application entry point."""
581
+
582
+ import uvicorn
583
+ from fastapi import FastAPI
584
+ from fastapi.middleware.cors import CORSMiddleware
585
+ from src.config.settings import settings
586
+ from src.api.router.routers import include_routers
587
+ from src.api.middlewares.security import setup_security_middleware
588
+ from src.api.middlewares.logging import setup_logging_middleware
589
+ from src.api.middlewares.error_handler import setup_error_handler
590
+ from src.api.middlewares.observability import setup_observability_middleware
591
+
592
+
593
+ def create_app() -> FastAPI:
594
+ """Create and configure FastAPI application."""
595
+ app = FastAPI(
596
+ title=settings.PROJECT_NAME,
597
+ version="0.1.0",
598
+ docs_url=None,
599
+ redoc_url=None,
600
+ )
601
+
602
+ setup_security_middleware(app)
603
+ setup_logging_middleware(app)
604
+ setup_error_handler(app)
605
+ setup_observability_middleware(app)
606
+
607
+ app.add_middleware(
608
+ CORSMiddleware,
609
+ allow_origins=["*"],
610
+ allow_credentials=True,
611
+ allow_methods=["*"],
612
+ allow_headers=["*"],
613
+ )
614
+
615
+ include_routers(app)
616
+
617
+ try:
618
+ from scalar_fastapi import get_scalar_api_reference
619
+
620
+ @app.get("/docs", include_in_schema=False)
621
+ async def scalar_html():
622
+ return get_scalar_api_reference(
623
+ openapi_url=app.openapi_url,
624
+ title=app.title,
625
+ )
626
+ except ImportError:
627
+ pass
628
+
629
+ return app
630
+
631
+
632
+ app = create_app()
633
+
634
+
635
+ if __name__ == "__main__":
636
+ uvicorn.run(
637
+ "src.api.main:app",
638
+ host=settings.SERVER_HOST,
639
+ port=settings.SERVER_PORT,
640
+ reload=settings.DEBUG,
641
+ )
642
+ '''
643
+
644
+ def _dependencies_py(self) -> str:
645
+ return '''"""Dependencies for v1 endpoints."""
646
+
647
+ from fastapi import Depends
648
+
649
+
650
+ def get_db_session():
651
+ """Get database session."""
652
+ pass
653
+ '''
654
+
655
+ def _health_py(self) -> str:
656
+ return '''"""Health check endpoint."""
657
+
658
+ from fastapi import APIRouter
659
+ from src.api.endpoints.v1.schemas.base import AppResponse
660
+
661
+ router = APIRouter()
662
+
663
+
664
+ @router.get("/health")
665
+ async def health_check():
666
+ """Health check endpoint."""
667
+ return AppResponse(success=True, data={"status": "healthy"})
668
+ '''
669
+
670
+ def _metrics_py(self) -> str:
671
+ return '''"""Prometheus metrics endpoint."""
672
+
673
+ from fastapi import APIRouter, Response
674
+ from src.observability.metrics import generate_metrics
675
+
676
+ router = APIRouter()
677
+
678
+
679
+ @router.get("/metrics")
680
+ async def metrics():
681
+ """Expose Prometheus metrics."""
682
+ metrics_data = generate_metrics()
683
+ return Response(content=metrics_data, media_type="text/plain")
684
+ '''
685
+
686
+ def _routers_py(self) -> str:
687
+ return '''"""Main v1 router aggregation."""
688
+
689
+ from fastapi import APIRouter
690
+ from src.api.endpoints.v1 import health, metrics, sample_agent, sample_di
691
+
692
+ v1_router = APIRouter(prefix="/api/v1")
693
+
694
+ v1_router.include_router(health.router, tags=["Health"])
695
+ v1_router.include_router(metrics.router, tags=["Metrics"])
696
+ v1_router.include_router(sample_agent.router, tags=["Agent"])
697
+ v1_router.include_router(sample_di.router, tags=["DI Example"])
698
+ '''
699
+
700
+ def _sample_agent_py(self) -> str:
701
+ return '''"""Sample agent endpoint."""
702
+
703
+ from fastapi import APIRouter
704
+ from src.api.endpoints.v1.schemas.base import AppResponse
705
+ from src.execution.usecases.sample_usecase import SampleUsecase
706
+
707
+ router = APIRouter()
708
+
709
+
710
+ @router.post("/agent/sample")
711
+ async def sample_agent(query: str):
712
+ """Sample agent endpoint."""
713
+ usecase = SampleUsecase()
714
+ result = await usecase.execute(query)
715
+ return AppResponse(success=True, data=result)
716
+ '''
717
+
718
+ def _sample_di_py(self) -> str:
719
+ return '''"""Sample dependency injection endpoint."""
720
+
721
+ from fastapi import APIRouter, Depends
722
+ from src.api.endpoints.v1.schemas.base import AppResponse
723
+ from src.api.endpoints.v1.dependencies import get_db_session
724
+
725
+ router = APIRouter()
726
+
727
+
728
+ @router.get("/di/sample")
729
+ async def sample_di(session=Depends(get_db_session)):
730
+ """Sample DI endpoint."""
731
+ return AppResponse(success=True, data={"message": "DI working"})
732
+ '''
733
+
734
+ def _schema_base_py(self) -> str:
735
+ return '''"""Base response schemas."""
736
+
737
+ from pydantic import BaseModel
738
+ from typing import Any, Optional
739
+ from uuid import UUID, uuid4
740
+
741
+
742
+ class ErrorDetail(BaseModel):
743
+ code: str
744
+ message: str
745
+ details: Optional[dict[str, Any]] = None
746
+
747
+
748
+ class AppResponse(BaseModel):
749
+ success: bool
750
+ data: Optional[Any] = None
751
+ error: Optional[ErrorDetail] = None
752
+ request_id: UUID = uuid4()
753
+ '''
754
+
755
+ def _schema_sample_py(self) -> str:
756
+ return '''"""Sample request/response schemas."""
757
+
758
+ from pydantic import BaseModel
759
+
760
+
761
+ class SampleRequest(BaseModel):
762
+ query: str
763
+
764
+
765
+ class SampleResponse(BaseModel):
766
+ message: str
767
+ result: str
768
+ '''
769
+
770
+ def _error_handler_py(self) -> str:
771
+ return '''"""Error handler middleware."""
772
+
773
+ from fastapi import FastAPI, Request
774
+ from fastapi.responses import JSONResponse
775
+ from src.core.exceptions import AppException
776
+ from src.api.endpoints.v1.schemas.base import AppResponse, ErrorDetail
777
+
778
+
779
+ def setup_error_handler(app: FastAPI):
780
+ """Setup global error handlers."""
781
+
782
+ @app.exception_handler(AppException)
783
+ async def app_exception_handler(request: Request, exc: AppException):
784
+ return JSONResponse(
785
+ status_code=exc.status_code,
786
+ content=AppResponse(
787
+ success=False,
788
+ error=ErrorDetail(
789
+ code=exc.error_code,
790
+ message=exc.message,
791
+ details=exc.details,
792
+ ),
793
+ ).model_dump(),
794
+ )
795
+ '''
796
+
797
+ def _logging_middleware_py(self) -> str:
798
+ return '''"""Request logging middleware."""
799
+
800
+ import logging
801
+ import time
802
+ from fastapi import FastAPI, Request
803
+ from starlette.middleware.base import BaseHTTPMiddleware
804
+
805
+ logger = logging.getLogger(__name__)
806
+
807
+
808
+ class LoggingMiddleware(BaseHTTPMiddleware):
809
+ async def dispatch(self, request: Request, call_next):
810
+ start_time = time.time()
811
+ response = await call_next(request)
812
+ duration = time.time() - start_time
813
+
814
+ logger.info(
815
+ f"{request.method} {request.url.path} - {response.status_code} - {duration:.3f}s"
816
+ )
817
+
818
+ return response
819
+
820
+
821
+ def setup_logging_middleware(app: FastAPI):
822
+ """Setup logging middleware."""
823
+ app.add_middleware(LoggingMiddleware)
824
+ '''
825
+
826
+ def _observability_middleware_py(self) -> str:
827
+ return '''"""Observability middleware for metrics and tracing."""
828
+
829
+ from fastapi import FastAPI, Request
830
+ from starlette.middleware.base import BaseHTTPMiddleware
831
+ from src.observability.metrics import metrics
832
+
833
+
834
+ class ObservabilityMiddleware(BaseHTTPMiddleware):
835
+ async def dispatch(self, request: Request, call_next):
836
+ response = await call_next(request)
837
+
838
+ metrics.counter(
839
+ "http_requests_total",
840
+ {"method": request.method, "endpoint": request.url.path, "status": str(response.status_code)},
841
+ )
842
+
843
+ return response
844
+
845
+
846
+ def setup_observability_middleware(app: FastAPI):
847
+ """Setup observability middleware."""
848
+ app.add_middleware(ObservabilityMiddleware)
849
+ '''
850
+
851
+ def _security_middleware_py(self) -> str:
852
+ return '''"""Security headers middleware."""
853
+
854
+ from fastapi import FastAPI, Request
855
+ from starlette.middleware.base import BaseHTTPMiddleware
856
+
857
+
858
+ class SecurityMiddleware(BaseHTTPMiddleware):
859
+ async def dispatch(self, request: Request, call_next):
860
+ response = await call_next(request)
861
+
862
+ response.headers["X-Content-Type-Options"] = "nosniff"
863
+ response.headers["X-Frame-Options"] = "DENY"
864
+ response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
865
+
866
+ return response
867
+
868
+
869
+ def setup_security_middleware(app: FastAPI):
870
+ """Setup security middleware."""
871
+ app.add_middleware(SecurityMiddleware)
872
+ '''
873
+
874
+ def _router_py(self) -> str:
875
+ return '''"""Router aggregation."""
876
+
877
+ from fastapi import FastAPI
878
+ from src.api.endpoints.v1.routers import v1_router
879
+
880
+
881
+ def include_routers(app: FastAPI):
882
+ """Include all routers in the application."""
883
+ app.include_router(v1_router)
884
+ '''
885
+
886
+ def _sample_action_py(self) -> str:
887
+ return '''"""Sample action."""
888
+
889
+ from typing import Any
890
+
891
+
892
+ class SampleAction:
893
+ """Sample action that performs a discrete operation."""
894
+
895
+ async def execute(self, query: str) -> dict[str, Any]:
896
+ """Execute the action."""
897
+ return {"message": f"Processed: {query}", "status": "success"}
898
+ '''
899
+
900
+ def _sample_usecase_py(self) -> str:
901
+ return '''"""Sample usecase."""
902
+
903
+ from typing import Any
904
+ from src.execution.actions.sample_action import SampleAction
905
+
906
+
907
+ class SampleUsecase:
908
+ """Sample usecase that orchestrates business flow."""
909
+
910
+ def __init__(self):
911
+ self.action = SampleAction()
912
+
913
+ async def execute(self, query: str) -> dict[str, Any]:
914
+ """Execute the usecase."""
915
+ result = await self.action.execute(query)
916
+ return {"usecase_result": result}
917
+ '''
918
+
919
+ def _agent_py(self) -> str:
920
+ return '''"""Agent definitions."""
921
+
922
+ from langchain_openai import ChatOpenAI
923
+ from src.config.settings import settings
924
+
925
+
926
+ class SampleAgent:
927
+ """Sample AI agent."""
928
+
929
+ def __init__(self):
930
+ self.llm = ChatOpenAI(
931
+ model=settings.OPENAI_MODEL_BASIC,
932
+ api_key=settings.OPENAI_API_KEY,
933
+ )
934
+
935
+ async def process(self, query: str) -> str:
936
+ """Process a query using the agent."""
937
+ response = await self.llm.ainvoke(query)
938
+ return response.content
939
+ '''
940
+
941
+ def _sample_prompt_py(self) -> str:
942
+ return '''"""Sample agent prompt template."""
943
+
944
+ SAMPLE_AGENT_PROMPT = """You are a helpful AI assistant.
945
+
946
+ Query: {query}
947
+
948
+ Please provide a helpful response."""
949
+ '''
950
+
951
+ def _model_loader_py(self) -> str:
952
+ return '''"""AI model loader provider."""
953
+
954
+ from langchain_openai import ChatOpenAI
955
+ from src.config.settings import settings
956
+
957
+
958
+ def get_llm(model: str = "basic") -> ChatOpenAI:
959
+ """Get LLM instance."""
960
+ model_name = (
961
+ settings.OPENAI_MODEL_REASONING
962
+ if model == "reasoning"
963
+ else settings.OPENAI_MODEL_BASIC
964
+ )
965
+
966
+ return ChatOpenAI(
967
+ model=model_name,
968
+ api_key=settings.OPENAI_API_KEY,
969
+ )
970
+ '''
971
+
972
+ def _observability_init_py(self) -> str:
973
+ return '''"""Observability module."""
974
+
975
+ from src.observability.metrics import metrics
976
+ from src.observability.tracing import tracer
977
+
978
+ __all__ = ["metrics", "tracer"]
979
+ '''
980
+
981
+ def _metrics_module_py(self) -> str:
982
+ return '''"""Prometheus metrics."""
983
+
984
+ from prometheus_client import Counter, Histogram, generate_latest
985
+
986
+
987
+ metrics_counter = Counter("http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"])
988
+
989
+
990
+ class MetricsCollector:
991
+ """Collect and expose metrics."""
992
+
993
+ def counter(self, name: str, labels: dict[str, str]):
994
+ """Increment a counter metric."""
995
+ pass
996
+
997
+ def generate(self) -> bytes:
998
+ """Generate metrics data."""
999
+ return generate_latest()
1000
+
1001
+
1002
+ metrics = MetricsCollector()
1003
+
1004
+
1005
+ def generate_metrics() -> bytes:
1006
+ """Generate Prometheus metrics."""
1007
+ return generate_latest()
1008
+ '''
1009
+
1010
+ def _tracing_py(self) -> str:
1011
+ return '''"""OpenTelemetry tracing."""
1012
+
1013
+ from contextlib import contextmanager
1014
+
1015
+
1016
+ class Tracer:
1017
+ """Simple tracer for distributed tracing."""
1018
+
1019
+ @contextmanager
1020
+ def start_as_current_span(self, name: str):
1021
+ """Start a new span."""
1022
+ yield
1023
+
1024
+
1025
+ tracer = Tracer()
1026
+ '''
1027
+
1028
+ def _db_connection_py(self) -> str:
1029
+ return '''"""Database connection setup."""
1030
+
1031
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
1032
+ from sqlalchemy.orm import sessionmaker
1033
+ from src.config.settings import settings
1034
+
1035
+
1036
+ engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
1037
+
1038
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
1039
+
1040
+
1041
+ async def get_db_session() -> AsyncSession:
1042
+ """Get database session."""
1043
+ async with async_session() as session:
1044
+ yield session
1045
+ '''
1046
+
1047
+ def _repository_base_py(self) -> str:
1048
+ return '''"""Base repository pattern."""
1049
+
1050
+ from typing import Generic, TypeVar, Type, Optional, Sequence
1051
+ from sqlalchemy import select
1052
+ from sqlalchemy.ext.asyncio import AsyncSession
1053
+
1054
+ ModelType = TypeVar("ModelType")
1055
+
1056
+
1057
+ class BaseRepository(Generic[ModelType]):
1058
+ """Base repository with common CRUD operations."""
1059
+
1060
+ def __init__(self, model: Type[ModelType]):
1061
+ self.model = model
1062
+
1063
+ async def get(self, session: AsyncSession, id: int) -> Optional[ModelType]:
1064
+ """Get by ID."""
1065
+ result = await session.execute(select(self.model).where(self.model.id == id))
1066
+ return result.scalar_one_or_none()
1067
+
1068
+ async def get_multi(
1069
+ self, session: AsyncSession, skip: int = 0, limit: int = 100
1070
+ ) -> Sequence[ModelType]:
1071
+ """Get multiple records."""
1072
+ result = await session.execute(
1073
+ select(self.model).offset(skip).limit(limit)
1074
+ )
1075
+ return result.scalars().all()
1076
+ '''
1077
+
1078
+ def _sqlalchemy_repo_py(self) -> str:
1079
+ return '''"""SQLAlchemy repository implementation."""
1080
+
1081
+ from src.database.repositories.base import BaseRepository
1082
+
1083
+
1084
+ class SQLAlchemyRepository(BaseRepository):
1085
+ """SQLAlchemy specific repository implementation."""
1086
+ pass
1087
+ '''
1088
+
1089
+ def _exceptions_py(self) -> str:
1090
+ return '''"""Custom exceptions."""
1091
+
1092
+ from typing import Optional
1093
+
1094
+
1095
+ class AppException(Exception):
1096
+ """Base application exception."""
1097
+
1098
+ def __init__(
1099
+ self,
1100
+ message: str,
1101
+ error_code: str,
1102
+ status_code: int = 500,
1103
+ details: Optional[dict] = None,
1104
+ ):
1105
+ self.message = message
1106
+ self.error_code = error_code
1107
+ self.status_code = status_code
1108
+ self.details = details or {}
1109
+ super().__init__(message)
1110
+
1111
+
1112
+ class NotFoundException(AppException):
1113
+ """Resource not found."""
1114
+
1115
+ def __init__(self, message: str, error_code: str = "NOT_FOUND", details: Optional[dict] = None):
1116
+ super().__init__(message, error_code, 404, details)
1117
+
1118
+
1119
+ class ValidationException(AppException):
1120
+ """Validation error."""
1121
+
1122
+ def __init__(self, message: str, error_code: str = "VALIDATION_ERROR", details: Optional[dict] = None):
1123
+ super().__init__(message, error_code, 422, details)
1124
+ '''
1125
+
1126
+ def _settings_py(self) -> str:
1127
+ return '''"""Application settings."""
1128
+
1129
+ from pydantic_settings import BaseSettings
1130
+ from functools import lru_cache
1131
+
1132
+
1133
+ class Settings(BaseSettings):
1134
+ """Application settings from environment variables."""
1135
+
1136
+ PROJECT_NAME: str = "ALMS Backend"
1137
+ DEBUG: bool = True
1138
+ SERVER_HOST: str = "0.0.0.0"
1139
+ SERVER_PORT: int = 3000
1140
+
1141
+ OPENAI_API_KEY: str = ""
1142
+ OPENAI_MODEL_BASIC: str = "gpt-4o-mini"
1143
+ OPENAI_MODEL_REASONING: str = "gpt-4o"
1144
+
1145
+ DATABASE_HOST: str = "localhost"
1146
+ DATABASE_PORT: int = 5432
1147
+ DATABASE_NAME: str = "mydb"
1148
+ DATABASE_USER: str = "postgres"
1149
+ DATABASE_PASSWORD: str = "secret"
1150
+
1151
+ REDIS_HOST: str = "localhost"
1152
+ REDIS_PORT: int = 6379
1153
+
1154
+ OTLP_ENDPOINT: str = "http://localhost:4317"
1155
+ METRICS_ENABLED: bool = True
1156
+ TRACING_ENABLED: bool = True
1157
+
1158
+ LOG_LEVEL: str = "info"
1159
+
1160
+ @property
1161
+ def DATABASE_URL(self) -> str:
1162
+ return f"postgresql+asyncpg://{self.DATABASE_USER}:{self.DATABASE_PASSWORD}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
1163
+
1164
+ @property
1165
+ def REDIS_URL(self) -> str:
1166
+ return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/0"
1167
+
1168
+ class Config:
1169
+ env_file = ".env"
1170
+ case_sensitive = True
1171
+
1172
+
1173
+ @lru_cache()
1174
+ def get_settings() -> Settings:
1175
+ """Get cached settings instance."""
1176
+ return Settings()
1177
+
1178
+
1179
+ settings = get_settings()
1180
+ '''
1181
+
1182
+ def _logs_config_py(self) -> str:
1183
+ return '''"""Logging configuration."""
1184
+
1185
+ import logging
1186
+ import sys
1187
+ from src.config.settings import settings
1188
+
1189
+
1190
+ def setup_logging():
1191
+ """Configure application logging."""
1192
+ log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
1193
+
1194
+ logging.basicConfig(
1195
+ level=log_level,
1196
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
1197
+ handlers=[logging.StreamHandler(sys.stdout)],
1198
+ )
1199
+ '''
1200
+
1201
+ def _conftest_py(self) -> str:
1202
+ return '''"""Pytest configuration and fixtures."""
1203
+
1204
+ import pytest
1205
+ from httpx import AsyncClient
1206
+ from src.api.main import app
1207
+
1208
+
1209
+ @pytest.fixture
1210
+ async def client():
1211
+ """Create test client."""
1212
+ async with AsyncClient(app=app, base_url="http://test") as client:
1213
+ yield client
1214
+ '''
1215
+
1216
+ def _test_health_py(self) -> str:
1217
+ return '''"""Health endpoint tests."""
1218
+
1219
+ import pytest
1220
+
1221
+
1222
+ @pytest.mark.asyncio
1223
+ async def test_health_check(client):
1224
+ """Test health check endpoint."""
1225
+ response = await client.get("/api/v1/health")
1226
+ assert response.status_code == 200
1227
+ data = response.json()
1228
+ assert data["success"] is True
1229
+ '''
1230
+
1231
+ def _test_agent_py(self) -> str:
1232
+ return '''"""Agent endpoint tests."""
1233
+
1234
+ import pytest
1235
+
1236
+
1237
+ @pytest.mark.asyncio
1238
+ async def test_sample_agent(client):
1239
+ """Test sample agent endpoint."""
1240
+ response = await client.post("/api/v1/agent/sample", json={"query": "test"})
1241
+ assert response.status_code == 200
1242
+ '''
1243
+
1244
+ def _test_metrics_py(self) -> str:
1245
+ return '''"""Metrics tests."""
1246
+
1247
+ import pytest
1248
+
1249
+
1250
+ @pytest.mark.asyncio
1251
+ async def test_metrics_collection():
1252
+ """Test metrics collection."""
1253
+ pass
1254
+ '''
1255
+
1256
+ def _test_metrics_endpoint_py(self) -> str:
1257
+ return '''"""Metrics endpoint tests."""
1258
+
1259
+ import pytest
1260
+
1261
+
1262
+ @pytest.mark.asyncio
1263
+ async def test_metrics_endpoint(client):
1264
+ """Test metrics endpoint."""
1265
+ response = await client.get("/api/v1/metrics")
1266
+ assert response.status_code == 200
1267
+ '''
1268
+
1269
+ def _test_observability_middleware_py(self) -> str:
1270
+ return '''"""Observability middleware tests."""
1271
+
1272
+ import pytest
1273
+
1274
+
1275
+ @pytest.mark.asyncio
1276
+ async def test_observability_middleware(client):
1277
+ """Test observability middleware."""
1278
+ response = await client.get("/api/v1/health")
1279
+ assert response.status_code == 200
1280
+ '''
1281
+
1282
+ def _test_sqlalchemy_repository_py(self) -> str:
1283
+ return '''"""SQLAlchemy repository tests."""
1284
+
1285
+ import pytest
1286
+
1287
+
1288
+ @pytest.mark.asyncio
1289
+ async def test_repository_base():
1290
+ """Test base repository."""
1291
+ pass
1292
+ '''
1293
+
1294
+ def _test_tracing_py(self) -> str:
1295
+ return '''"""Tracing tests."""
1296
+
1297
+ import pytest
1298
+
1299
+
1300
+ @pytest.mark.asyncio
1301
+ async def test_tracing():
1302
+ """Test tracing setup."""
1303
+ pass
1304
+ '''
1305
+
1306
+ def _test_full_stack_py(self) -> str:
1307
+ return '''"""Full stack integration tests."""
1308
+
1309
+ import pytest
1310
+
1311
+
1312
+ @pytest.mark.asyncio
1313
+ async def test_full_stack(client):
1314
+ """Test full stack integration."""
1315
+ response = await client.get("/api/v1/health")
1316
+ assert response.status_code == 200
1317
+ '''
1318
+
1319
+ def _test_workflows_py(self) -> str:
1320
+ return '''"""End-to-end workflow tests."""
1321
+
1322
+ import pytest
1323
+
1324
+
1325
+ @pytest.mark.asyncio
1326
+ async def test_workflow(client):
1327
+ """Test complete workflow."""
1328
+ pass
1329
+ '''