Skip to content

Commit aee6551

Browse files
authored
add Claude instructions and serve OpenAPI v3.0.1 spec for Azure API Management compatibility (#18)
* add Claude instructions and serve OpenAPI v3.0.1 spec for Azure API Management compatibility * refactors * fmt
1 parent 6d2dd39 commit aee6551

File tree

3 files changed

+149
-1
lines changed

3 files changed

+149
-1
lines changed

CLAUDE.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
TLM App is a standalone web application for serving the Trustworthy Language Model (TLM) using FastAPI. The main service is `chat-backend` which provides a REST API for chat functionality.
8+
9+
## Development Commands
10+
11+
### Running the Application Locally
12+
- **Setup**: Requires Docker, Tilt, and Kind
13+
- **Create cluster**: `kind create cluster --name tlm-app`
14+
- **Start development**: `tilt up`
15+
- **Access application**: http://localhost:8080
16+
- **API docs**: http://localhost:8080/api/docs
17+
18+
### Testing Commands
19+
- **Run all tests**: `./scripts/dev/test.sh chat-backend`
20+
- **Run specific test**: `./scripts/dev/test.sh chat-backend tests/routers/test_chat.py`
21+
- **Run with debugger**: `./scripts/dev/test.sh chat-backend --pdb`
22+
- **Run with stdout**: `./scripts/dev/test.sh chat-backend -s`
23+
24+
### Alternative Test Commands (via Makefile)
25+
- **Start test harness**: `make test-up`
26+
- **Run tests**: `make test-chat-backend`
27+
- **Stop test harness**: `make test-down`
28+
29+
## Architecture
30+
31+
### Core Stack
32+
- **FastAPI**: ASGI web framework with type annotations and auto-generated docs
33+
- **Pydantic**: Data validation and parsing
34+
- **uvicorn**: ASGI server for production
35+
- **Python 3.10+**: Required runtime
36+
37+
### Service Structure
38+
The main service is in `services/chat-backend/` with:
39+
- `src/app.py`: Main FastAPI application entry point
40+
- `src/routers/`: API endpoint definitions (chat, health, main)
41+
- `src/schemas/`: Pydantic models for request/response validation
42+
- `src/services/`: Business logic (Azure integration, chat config, streaming)
43+
- `src/middleware/`: Request/response processing (audit logging)
44+
- `src/utils/`: Utilities (logging, models configuration)
45+
- `tests/`: Test suite with pytest and async support
46+
47+
### Key Configuration
48+
- **Environment**: Copy `.env.example` to `.env` in service directories (values unquoted)
49+
- **TLM Core**: External dependency at version v0.0.39
50+
- **Code quality**: Ruff formatter (120 char line length), mypy type checking
51+
- **Test coverage**: Coverage reporting with pytest
52+
53+
### Deployment
54+
- **Kubernetes**: Helm charts in `deploy/helm/`
55+
- **Container registry**: Uses Docker build with live updates via Tilt
56+
- **Infrastructure**: Terraform configs for Azure (AKS) and AWS (EKS)
57+
58+
## Code Quality Tools
59+
- **Linting**: `ruff` (configured in pyproject.toml)
60+
- **Type checking**: `mypy` with Pydantic plugin
61+
- **Testing**: `pytest` with async support and recording for external calls
62+
- **SonarQube**: Automated code quality scanning (project: TLM-app)

services/chat-backend/src/app.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,29 @@
33
# setup logging, before importing anything else (and before creating other loggers)
44
setup_logging()
55

6+
import json
67
import logging
78

89
from fastapi import FastAPI, Request, Response # noqa: E402
910
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
11+
from fastapi.responses import JSONResponse # noqa: E402
1012
from fastapi.routing import APIRoute # noqa: E402
1113

1214
from src.core.config import get_settings # noqa: E402
1315
from src.middleware import AuditMiddleware # noqa: E402
1416
from src.routers.main import api_router # noqa: E402
17+
from src.utils.openapi_converter import convert_openapi_spec # noqa: E402
1518

1619
config = get_settings()
1720
logger = logging.getLogger(__name__)
1821

1922

2023
def custom_generate_unique_id(route: APIRoute) -> str:
21-
return f"{route.tags[0]}-{route.name}"
24+
if route.tags and len(route.tags) > 0:
25+
return f"{route.tags[0]}-{route.name}"
26+
return route.name
27+
28+
2229

2330

2431
app = FastAPI(
@@ -40,6 +47,15 @@ def custom_generate_unique_id(route: APIRoute) -> str:
4047
app.include_router(api_router, prefix=config.API_PREFIX)
4148

4249

50+
# Add OpenAPI 3.0.1 compatible endpoint
51+
@app.get(f"{config.API_PREFIX}/openapi-3.0.1.json", include_in_schema=False)
52+
async def get_openapi_3_0_1():
53+
"""Get OpenAPI spec converted to 3.0.1 for compatibility."""
54+
openapi_spec = app.openapi()
55+
converted_spec = convert_openapi_spec(openapi_spec)
56+
return JSONResponse(content=converted_spec)
57+
58+
4359
# Add logging on exceptions
4460
@app.exception_handler(Exception)
4561
async def exception_handler(request: Request, exc: Exception) -> Response:
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
OpenAPI 3.1.0 to 3.0.1 converter utility.
3+
"""
4+
5+
import json
6+
from typing import Any, Dict
7+
8+
9+
def convert_const_to_enum(schema: Dict[str, Any]) -> Dict[str, Any]:
10+
"""Convert OpenAPI 3.1 'const' to OpenAPI 3.0 'enum'."""
11+
if isinstance(schema, dict):
12+
if 'const' in schema:
13+
schema['enum'] = [schema.pop('const')]
14+
15+
# Recursively process nested schemas
16+
for value in schema.values():
17+
if isinstance(value, dict):
18+
convert_const_to_enum(value)
19+
elif isinstance(value, list):
20+
for item in value:
21+
if isinstance(item, dict):
22+
convert_const_to_enum(item)
23+
24+
return schema
25+
26+
27+
def convert_anyof_nullable(schema: Dict[str, Any]) -> Dict[str, Any]:
28+
"""Convert OpenAPI 3.1 anyOf nullable patterns to OpenAPI 3.0 nullable."""
29+
if isinstance(schema, dict):
30+
# Handle anyOf with null type pattern
31+
if 'anyOf' in schema:
32+
any_of = schema['anyOf']
33+
# Check if this is a nullable pattern (has null type)
34+
non_null_schemas = [s for s in any_of if s.get('type') != 'null']
35+
null_schemas = [s for s in any_of if s.get('type') == 'null']
36+
37+
if null_schemas and len(non_null_schemas) == 1:
38+
# This is a nullable pattern, convert it
39+
base_schema = non_null_schemas[0]
40+
schema.update(base_schema)
41+
schema['nullable'] = True
42+
schema.pop('anyOf')
43+
44+
# Recursively process nested schemas
45+
for value in schema.values():
46+
if isinstance(value, dict):
47+
convert_anyof_nullable(value)
48+
elif isinstance(value, list):
49+
for item in value:
50+
if isinstance(item, dict):
51+
convert_anyof_nullable(item)
52+
53+
return schema
54+
55+
56+
def convert_openapi_spec(spec: Dict[str, Any]) -> Dict[str, Any]:
57+
"""Convert OpenAPI 3.1.0 spec to 3.0.1."""
58+
# Make a copy to avoid modifying the original
59+
converted_spec = json.loads(json.dumps(spec))
60+
61+
# Change version
62+
converted_spec['openapi'] = '3.0.1'
63+
64+
# Convert const to enum
65+
convert_const_to_enum(converted_spec)
66+
67+
# Convert anyOf nullable patterns
68+
convert_anyof_nullable(converted_spec)
69+
70+
return converted_spec

0 commit comments

Comments
 (0)