Production-ready multi-tenant AI chatbot backend built with FastAPI and Amazon Bedrock.
greentiq-backend/
|-- app/
| |-- main.py # FastAPI app entry point
| |-- config.py # Settings loaded from environment variables
| |-- core/
| | |-- dependencies.py # Dependency injection for singleton services
| | |-- exceptions.py # Custom HTTP exceptions
| | `-- logging.py # Structured JSON logging
| |-- models/
| | `-- schemas.py # Pydantic request and response models
| |-- routes/
| | |-- chat.py # POST /chat
| | |-- ingest.py # POST /ingest, POST /ingest/file
| | `-- tenant.py # Tenant management endpoints
| `-- services/
| |-- agent_service.py # Agent persona prompt selection
| |-- bedrock_service.py # Bedrock runtime and KB API wrapper
| |-- conversation_service.py # Session history storage
| |-- rag_service.py # Retrieval-augmented generation pipeline
| |-- s3_service.py # Tenant-isolated S3 uploads
| |-- tenant_service.py # Tenant lookup and caching
| `-- usage_service.py # Token usage tracking
|-- tests/
|-- .env.example
|-- .gitignore
|-- requirements.txt
`-- README.md
Before running the backend, set up the following AWS resources.
Enable these models in Bedrock model access:
- Anthropic Claude 3 Sonnet
- Amazon Titan Text Embeddings V2
Example:
Bucket name: greentiq-dev-bucket
Region: us-east-1
Block all public access: ON
All tenant documents are stored under tenant-specific prefixes such as acme-corp/docs/.
Example:
Name: greentiq-kb-acme-corp
Embedding model: Amazon Titan Text Embeddings V2
Data source: s3://greentiq-dev-bucket/acme-corp/docs/
Vector store: OpenSearch Serverless collection
Save the Knowledge Base ID and Data Source ID for tenant registration.
The backend can run locally without these tables, but persistence is limited.
Table 1: greentiq-tenants-dev
Partition key: tenant_id (String)
Table 2: greentiq-usage-dev
Partition key: tenant_id (String)
Sort key: request_id (String)
Table 3: greentiq-conversations-dev
Partition key: tenant_id (String)
Sort key: session_id (String)
Notes:
- Without the tenant table, registered tenants only live in memory for the current process.
- Without the usage table,
/tenants/{tenant_id}/usagecannot persist usage data. - Without the conversations table, chat memory falls back to in-process memory and is lost on restart.
# 1. Clone the project
git clone https://github.com/your-org/greentiq-backend.git
cd greentiq-backend
# 2. Create and activate a virtual environment
python -m venv venv
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure environment variables
cp .env.example .env
# 5. Configure AWS CLI credentials if needed
aws configure
# 6. Run the API
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Server URL: http://localhost:8000
OpenAPI docs: http://localhost:8000/docs
curl -X POST http://localhost:8000/tenants/register \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"knowledge_base_id": "YOUR_KB_ID",
"data_source_id": "YOUR_DS_ID",
"custom_system_prompt": null
}'Example response:
{
"tenant_id": "acme-corp",
"knowledge_base_id": "XYZABC123",
"data_source_id": "DS456DEF",
"s3_prefix": "acme-corp/docs/",
"custom_system_prompt": null,
"created_at": "2026-03-27T10:00:00+00:00"
}curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"source_type": "text",
"source_value": "Our return policy allows returns within 30 days of purchase."
}'curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"source_type": "website",
"source_value": "https://www.acmecorp.com/support"
}'Website ingestion notes:
- Only valid
http://andhttps://URLs are accepted. - If the site cannot be fetched, the API returns
502instead of ingesting an error string. - If no readable text can be extracted, the API returns
422.
curl -X POST http://localhost:8000/ingest/file \
-F "tenant_id=acme-corp" \
-F "file=@/path/to/product-manual.pdf"Accepted file types:
- TXT
- DOC
- DOCX
- HTML
- Markdown
File upload limits:
- Maximum size: 50 MB
- Unsupported file types return
415
Example response:
{
"tenant_id": "acme-corp",
"job_id": "ingest-job-abc123",
"status": "accepted",
"message": "Content uploaded to S3. Knowledge base sync started in background. Use GET /ingest/status to check progress.",
"s3_key": "acme-corp/docs/text_20260327_abc12345.txt"
}curl "http://localhost:8000/ingest/status?tenant_id=acme-corp&job_id=ingest-job-abc123"Example response:
{
"tenant_id": "acme-corp",
"job_id": "ingest-job-abc123",
"status": "COMPLETE",
"complete": true
}Wait for "complete": true before sending chat requests.
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"agent_type": "support",
"message": "What is your return policy?"
}'curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"agent_type": "sales",
"message": "Why should I choose your product?"
}'# First message - omit session_id to start a new session
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"agent_type": "support",
"message": "What plans do you offer?"
}'
# Reuse the returned session_id for follow-up messages
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "acme-corp",
"agent_type": "support",
"message": "Tell me more about the second one",
"session_id": "SESSION_ID_FROM_PREVIOUS_RESPONSE"
}'Conversation behavior:
- If
session_idis omitted, the backend creates one automatically. - Conversation history is persisted in DynamoDB when
greentiq-conversations-devexists. - Without the conversation table, memory still works within the current process.
Example response:
{
"tenant_id": "acme-corp",
"agent_type": "support",
"response": "Our return policy allows returns within 30 days of purchase.",
"session_id": "sess_acme-corp_f3a91c2d4e5f",
"tokens_used": {
"input_tokens": 420,
"output_tokens": 85,
"total_tokens": 505
},
"source_chunks_used": 3,
"fallback_used": false
}If fallback_used is true, the model did not find enough relevant KB context above the configured relevance threshold.
curl -X PUT http://localhost:8000/tenants/acme-corp/prompt \
-H "Content-Type: application/json" \
-d '{
"prompt": "You are Aria, Acme Corp'\''s friendly AI assistant. Always be warm, professional, and concise."
}'Then send chat requests with "agent_type": "custom".
Prompt update notes:
- Prompt changes update the in-memory tenant cache immediately.
- If DynamoDB is unavailable in local development, the custom prompt still works for the current process.
curl http://localhost:8000/tenants/acme-corp/usageExample response:
{
"tenant_id": "acme-corp",
"total_requests": 47,
"total_input_tokens": 19740,
"total_output_tokens": 3995,
"total_tokens": 23735
}# Run all tests
pytest tests/ -v
# Quieter output
pytest tests/ -q| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /tenants/register |
Register a new tenant |
| GET | /tenants/{tenant_id} |
Get tenant config |
| PUT | /tenants/{tenant_id}/prompt |
Update custom agent prompt |
| GET | /tenants/{tenant_id}/usage |
Get token usage stats |
| POST | /chat |
Send a chat message |
| POST | /ingest |
Ingest text or website content |
| POST | /ingest/file |
Upload a tenant document |
| GET | /ingest/status |
Check ingestion job status |
| GET | /ingest/files |
List ingested tenant files |
| Variable | Default | Description |
|---|---|---|
AWS_REGION |
us-east-1 |
AWS region |
S3_BUCKET_NAME |
greentiq-dev-bucket |
Shared S3 bucket for tenant documents |
BEDROCK_MODEL_ID |
anthropic.claude-3-sonnet-20240229-v1:0 |
Default Bedrock chat model |
BEDROCK_EMBED_MODEL_ID |
amazon.titan-embed-text-v2:0 |
Embedding model for KBs |
BEDROCK_MAX_TOKENS |
1024 |
Max output tokens per response |
BEDROCK_TOP_K_RESULTS |
5 |
Number of KB chunks to retrieve |
BEDROCK_RELEVANCE_THRESHOLD |
0.4 |
Minimum retrieval score to use a chunk |
DYNAMODB_TENANT_TABLE |
greentiq-tenants-dev |
Tenant config table |
DYNAMODB_USAGE_TABLE |
greentiq-usage-dev |
Usage tracking table |
DYNAMODB_CONVERSATION_TABLE |
greentiq-conversations-dev |
Conversation memory table |
DEBUG |
false |
Enables verbose error details |
DEBUG also accepts common deployment values such as dev, debug, release, and production.
"Tenant not found"
Register the tenant first with POST /tenants/register.
"Bedrock error: AccessDeniedException"
Your AWS principal does not have sufficient Bedrock permissions.
fallback_used: true on every response
The KB may still be syncing, or the relevance threshold is too high. Check /ingest/status and consider lowering BEDROCK_RELEVANCE_THRESHOLD.
"Unsupported file type"
Use one of the accepted file formats for /ingest/file: PDF, TXT, DOC, DOCX, HTML, or Markdown.
"Failed to fetch website content"
Confirm the URL is reachable over HTTP or HTTPS from the backend environment.
Conversation memory disappears after restart
Create the greentiq-conversations-dev table so session history persists outside process memory.
Usage totals are always zero in dev
Create the usage table, or confirm your AWS credentials allow DynamoDB access.
uvicorn app.main:app --reload