Prompt Suggestion — Service Documentation
Overview
The prompt-suggestion service generates suggested prompts for brands. It is a
self-contained FastAPI microservice that lives entirely inside
apps/prompt_suggestion_service/.
Given a brand's context the service:
- Retrieves the top-N most semantically similar reference prompts from a vector database, filtered by industry, intent tag, and branded tag.
- Synthesizes clean, well-formed suggested prompts by sending a rendered
prompt template to the LLM microservice in
apps/ai-serviceand parsing the structured JSON output.
Both steps are exposed through POST /suggestions. The apps/ai-service LLM
microservice is intentionally a thin completion wrapper: it accepts a prompt
string and returns the raw output string. All template construction, output
parsing, and tag-modifier logic lives in this service — apps/ai-service
never sees the brand schema, the references, or the structured response
shape.
This service covers the request-time system only. The offline pipeline that ingests, cleanses, embeds, and clusters the 50M-prompt clickstream dataset is a separate workstream and is not implemented here.
Scope
Everything in this project lives in apps/prompt_suggestion_service/. External
services (e.g. apps/server) are consumers — they call POST /suggestions
and receive the final generated prompts.
Endpoints:
POST /suggestions— primary endpoint; retrieves references and synthesizes prompts in one call. External consumers only ever call this.POST /retrieve— internal helper for testing; returns top-N references only.POST /synthesize— internal helper for testing; takes a supplied list of references and returns synthesized prompts.GET /health— liveness check.
Out of scope:
- The actual LLM call (delegated to
apps/ai-service) - The offline ingestion, cleansing, clustering, and embedding pipeline
- Any frontend or UI
- Database migrations — the service is intended to be read-only against
the shared PostgreSQL database; schema ownership and migrations belong to
apps/servervia Prisma - Authentication (removed for now; reintroduce once the scheme is decided)
- Structured logging / request middleware / readiness checks (removed for now; reintroduce as needed)
Architecture
[External Consumer (e.g. apps/server)]
│ POST /suggestions
▼
[apps/prompt_suggestion_service (FastAPI)]
│
├──► [SuggestionService]
│ │
│ ├──► [Embedder] ──► [OpenAI Embeddings (default)]
│ ├──► [VectorStore] ──► [pgvector (default, currently a stub)]
│ └──► [LLMOrchestrator]
│ ├──► [PromptBuilder + tag_modifiers] (local)
│ ├──► HTTP ──► [apps/ai-service /complete]
│ └──► [SuggestedPromptListParser] (local)
│
└── /retrieve → Embedder + VectorStore only
└── /synthesize → LLMOrchestrator only
Request flow for POST /suggestions:
- Consumer sends
{brand, industry, existing_prompts?, tag_filters?, limit?}. SuggestionServiceembeds"{brand} {industry}"via theEmbedderand callsVectorStore.similarity_search(...)to pull top-N references (currentlylimit * 5).SuggestionServicecallsLLMOrchestrator.synthesize(...):PromptBuilder.build(...)renders the template with brand context, reference list, and any tag-modifier fragments.- The orchestrator POSTs
{"prompt": <rendered>}to{AI_SERVICE_URL}/complete. SuggestedPromptListParser.parse(...)extracts the JSON array from the completion text and validates it againstSuggestedPrompt.- Response:
{suggestions: [...], meta: {reference_count, llm_model, duration_ms}}.
When LLM_PROVIDER=mock, the orchestrator short-circuits to deterministic
dummy prompts and never opens an HTTP connection — useful for local dev when
apps/ai-service isn't running.
Directory structure
apps/prompt_suggestion_service/
├── pyproject.toml # uv-managed; single source of truth for deps
├── Dockerfile
├── .env.example
├── docs/
│ └── documentation.md # This file
└── app/ # Python package root
├── __init__.py
├── main.py # FastAPI app + router registration + /health
├── settings.py # pydantic-settings config — use this, not os.getenv
├── schemas/ # Pydantic v2 request/response models
│ ├── __init__.py
│ ├── shared.py # IntentTag, BrandedTag, TagFilter, HealthResponse
│ ├── retrieval.py # RetrieveRequest, ReferencePrompt, RetrieveResponse
│ ├── synthesis.py # SynthesizeRequest, SuggestedPrompt, SynthesizeResponse
│ └── suggestions.py # SuggestionRequest, ResponseMeta, SuggestionResponse
├── api/ # FastAPI routers
│ ├── __init__.py
│ ├── suggestions.py # POST /suggestions ← primary external endpoint
│ ├── retrieve.py # POST /retrieve ← internal helper
│ └── synthesize.py # POST /synthesize ← internal helper
├── services/ # Business logic
│ ├── __init__.py
│ └── suggestion_service.py # Orchestrates retrieval + synthesis
├── adapters/ # Pluggable infrastructure interfaces
│ ├── __init__.py
│ ├── vector_store.py # VectorStore ABC + PgVectorStore (stub)
│ └── embedder.py # Embedder ABC + OpenAIEmbedder + NoOpEmbedder
└── llm/ # Synthesis composition (everything LLM-adjacent
# except the call itself)
├── __init__.py
├── tag_modifiers.py # IntentTag/BrandedTag → instruction fragments
├── prompt_builder.py # PromptBuilder.build() — base template assembly
├── output_parser.py # JSON output parser → list[SuggestedPrompt]
└── orchestrator.py # builder + httpx call + parser
LLM service contract
The LLM call is delegated to apps/ai-service. The current placeholder
contract — to be confirmed with the apps/ai-service team — is:
POST {AI_SERVICE_URL}/complete
Content-Type: application/json
Body: {"prompt": "<string>"}
Response: {"output": "<string>"}
Update _COMPLETE_PATH in app/llm/orchestrator.py (and this section) once
the real shape is known.
Data model
The service is intended to have read-only access to the shared PostgreSQL
database. The canonical schema is defined in
apps/server/prisma/schema.prisma; the Pydantic models in app/schemas/
mirror it for read access.
The DB connection is not yet wired — PgVectorStore.similarity_search()
currently returns an empty list. The connection pool and readiness checks
were removed during the early-stage cleanup and will be reintroduced when the
vector store implementation lands.
Tables
Topic — a single search dimension; prompts belong to at most one topic.
| Column | Type | Notes |
|---|---|---|
| id | String | cuid primary key |
| name | String | |
| industry | String | indexed |
| created_at | DateTime |
Cluster — groups semantically similar prompts; produced by the offline pipeline.
| Column | Type | Notes |
|---|---|---|
| id | String | cuid primary key |
| topic_id | String | FK → Topic |
| size | Int | number of prompts in cluster |
| dominant_intent | IntentTag | |
| dominant_branded | BrandedTag | |
| created_at | DateTime |
The cluster centroid embedding lives in the vector store, referenced by cluster id.
Prompt — a real cleansed clickstream prompt (used as a synthesis reference).
| Column | Type | Notes |
|---|---|---|
| id | String | cuid primary key |
| cluster_id | String | FK → Cluster |
| text | String | |
| intent_tag | IntentTag | |
| branded_tag | BrandedTag | |
| language | String | |
| frequency | Int | default 1 |
| is_truncated | Boolean | default false |
| created_at | DateTime |
Enums
IntentTag: INFORMATIONAL | COMMERCIAL | TRANSACTIONAL
BrandedTag: BRANDED | NON_BRANDED
Synthesis prompt design
Base prompt template (app/llm/prompt_builder.py)
You are generating clean, well-formed search prompts that real buyers would type
into AI search tools, based on real-user reference prompts from a clickstream.
Brand: {brand}
Industry: {industry}
Existing prompts already tracked:
{existing_prompts}
Reference prompts from real users (these may be messy, multilingual, or truncated):
{reference_prompts}
{tag_modifier_fragments}
Generate {n} distinct prompts. Each prompt must:
- Be well-formed and free of typos
- Represent a different angle or stage within the topic (research, comparison, decision)
- Not duplicate the existing tracked prompts
- Be in English
Return as a JSON array, one object per prompt:
[{"prompt": "...", "topic": "...", "intent_tag": "INFORMATIONAL|COMMERCIAL|TRANSACTIONAL", "branded_tag": "BRANDED|NON_BRANDED", "source_cluster_id": "..."}]
Tag modifier fragments (app/llm/tag_modifiers.py)
| Tag | Fragment injected into prompt |
|---|---|
| INFORMATIONAL | Phrase prompts as research-stage questions seeking information. |
| COMMERCIAL | Phrase prompts as comparison-stage queries evaluating options. |
| TRANSACTIONAL | Phrase prompts as decision-stage queries with clear buying intent. |
| BRANDED | Each prompt must mention the brand: {brand}. |
| NON_BRANDED | Prompts must NOT mention any brand name. |
Deferred decisions (⚠ research items for meta team)
-
Vector DB — Default: pgvector (no extra infra). Alternatives: Qdrant, Pinecone, Weaviate. Abstracted behind
VectorStoreinapp/adapters/vector_store.py. -
Embedding model — Default: OpenAI
text-embedding-3-small. Document embeddings are pre-computed offline; theEmbedderinterface is used only to embed the incoming query context at request time. Abstracted behindEmbedderinapp/adapters/embedder.py.EMBEDDING_PROVIDER=noneactivatesNoOpEmbedderfor local dev without an API key. -
LLM service endpoint — Placeholder is
POST {AI_SERVICE_URL}/completewith{"prompt": str}→{"output": str}. Confirm exact shape with the apps/ai-service team and update_COMPLETE_PATHinapp/llm/orchestrator.pyto match. -
Inter-service auth — Currently no auth. Confirm with security/meta team what scheme to use (API key, mTLS, OAuth2, …) before exposing publicly.
Environment variables
See .env.example for the full list with descriptions.
| Variable | Default | Purpose |
|---|---|---|
LLM_PROVIDER |
mock |
mock for dummy data; remote to call apps/ai-service over HTTP |
LLM_MODEL |
gpt-4o-mini |
Label echoed in response metadata |
AI_SERVICE_URL |
— | Base URL of apps/ai-service; required when LLM_PROVIDER=remote |
EMBEDDING_PROVIDER |
openai |
Embedding provider (openai or none) |
EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model identifier |
OPENAI_API_KEY |
— | Required when EMBEDDING_PROVIDER=openai |
VECTOR_STORE |
pgvector |
Vector store backend |
REQUEST_TIMEOUT_MS |
30000 |
Per-upstream-call timeout in ms (applied to the apps/ai-service POST) |
Common commands
# Sync dependencies
uv --project apps/prompt_suggestion_service sync
# Run locally (from repo root)
uv --project apps/prompt_suggestion_service run uvicorn app.main:app --reload --port 8000
# Run tests
uv --project apps/prompt_suggestion_service run pytest
# Lint + format check
uv --project apps/prompt_suggestion_service run ruff check apps/prompt_suggestion_service
uv --project apps/prompt_suggestion_service run ruff format --check apps/prompt_suggestion_service
FastAPI auto-generates docs at:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc - Raw JSON:
http://localhost:8000/openapi.json