Skip to content

feat: per-user system prompt via compacted user-settings topic - #47

Open
tsuz wants to merge 1 commit into
mainfrom
feat/user-settings
Open

feat: per-user system prompt via compacted user-settings topic#47
tsuz wants to merge 1 commit into
mainfrom
feat/user-settings

Conversation

@tsuz

@tsuz tsuz commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a per-user system prompt override, delivered through a new compacted Kafka topic and joined into the session context — mirroring the existing memoir pattern.

  • New topic {AGENT_NAME}-user-settings (compacted, keyed by user_id), value: {"system_prompt": "...", "updated_at": "..."}. A tombstone reverts the user to the default prompt.
  • Gated by ENABLE_USER_SETTING (default false):
    • true → the streams app creates the topic if missing, materializes it as a KTable, and left-joins it into FullSessionContext during enrichment.
    • false → the topology never references the topic and system_prompt stays null.
  • Think consumer: when system_prompt is present and non-blank it replaces the default base prompt (SYSTEM_PROMPT_FILE / built-in) entirely; memoir context is still appended on top.
  • Enrichment refactor: the stream re-keys to user_id once when either memoir or user settings is enabled, so the settings join works with MEMOIR_ENABLED=false and enabling both costs a single repartition.
  • Both FullSessionContext models gained a nullable system_prompt field (@JsonIgnoreProperties(ignoreUnknown=true) on both sides), so mixed old/new deployments are safe. The think-side record keeps a 7-arg compatibility constructor.

Settings update semantics

A KTable join uses the table state at message-flow time, so a settings update takes effect from the user's next turn.

Tests

  • UserSettingTopologyTest (new): flag on / on-without-memoir / off, including topology-description assertions that the store and topic are absent when disabled.
  • EnrichInputMessageProcessorTest: join, absent record, tombstone revert, per-user isolation, memoir+settings combined.
  • SystemPromptTest: override replaces default, null/blank falls back, override composes with memoir.
  • Full suites green: processing 107/107, think-consumer 40/40.

🤖 Generated with Claude Code

Gated by ENABLE_USER_SETTING (default false). When enabled, the streams
app creates the compacted {AGENT_NAME}-user-settings topic (keyed by
user_id), materializes it as a KTable, and left-joins it into
FullSessionContext during enrichment. The think consumer uses the
joined system_prompt as the base prompt in place of the default
(SYSTEM_PROMPT_FILE / built-in); memoir context is still appended on
top. A tombstone reverts the user to the default prompt.

The enrichment processor now re-keys to user_id once when either memoir
or user settings is enabled, so enabling both costs a single
repartition, and the settings join works with MEMOIR_ENABLED=false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional per-user system prompt override sourced from a new compacted Kafka topic, joins it into the session context during stream enrichment, and applies it in the think consumer as a full replacement for the default base prompt (while still appending memoir context).

Changes:

  • Introduces a new compacted *-user-settings topic (Topics.USER_SETTINGS) and a UserSettings model, with topology wiring gated by ENABLE_USER_SETTING.
  • Updates the processing enrichment topology to (optionally) left-join user settings by user_id and carry system_prompt through FullSessionContext.
  • Updates the think consumer to resolve the base prompt from the per-user override (null/blank fallback) and adds/extends tests for the new behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java Adds tests for per-user base prompt override resolution and composition with memoir.
think/think-consumer/src/main/java/io/flightdeck/think/model/FullSessionContext.java Adds nullable system_prompt to think-side context plus a compatibility constructor.
think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java Applies system_prompt override via resolveBasePrompt() when building the system prompt.
processor-apps/processing/src/test/java/io/flightdeck/streams/UserSettingTopologyTest.java New topology-level tests ensuring user-settings wiring is present/absent based on the flag.
processor-apps/processing/src/test/java/io/flightdeck/streams/processors/EnrichInputMessageProcessorTest.java Adds join semantics tests (present/absent/tombstone/isolation/memoir+settings).
processor-apps/processing/src/main/java/io/flightdeck/streams/processors/EnrichInputMessageProcessor.java Adds optional user-settings join keyed by user_id and enriches FullSessionContext.system_prompt.
processor-apps/processing/src/main/java/io/flightdeck/streams/model/UserSettings.java New model for compacted per-user settings records.
processor-apps/processing/src/main/java/io/flightdeck/streams/model/FullSessionContext.java Adds nullable system_prompt to processing-side context.
processor-apps/processing/src/main/java/io/flightdeck/streams/FlightDeckStreamsApp.java Adds ENABLE_USER_SETTING gating, topic creation, and KTable materialization.
processor-apps/processing/src/main/java/io/flightdeck/streams/config/Topics.java Adds the USER_SETTINGS topic constant and documentation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +44 to +48
void resolveBasePrompt_nullOrBlank_usesDefault() {
assertThat(ThinkConsumer.resolveBasePrompt(null)).contains("intelligent AI assistant");
assertThat(ThinkConsumer.resolveBasePrompt("")).contains("intelligent AI assistant");
assertThat(ThinkConsumer.resolveBasePrompt(" ")).contains("intelligent AI assistant");
}
Comment on lines 116 to +118
full.history().size(),
full.memoirContext() != null,
full.systemPrompt() != null,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants