Files
LocalDiplomacy/docs/LocalDiplomacy_PLAN.md
T

760 lines
22 KiB
Markdown

# LocalDiplomacy Architecture Plan
## Summary
LocalDiplomacy is a Mount & Blade II: Bannerlord singleplayer mod that keeps the roleplay and diplomacy feel of AIInfluence while moving AI orchestration out of the game process.
The game mod should be a thin, safe client. It captures compact world and dialogue context from Bannerlord, displays AI-driven conversations and events, validates proposed actions, and executes only whitelisted mutations. The external `LocalDiplomacy.Agent` service owns prompt construction, memory retrieval, KoboldCpp calls, tool execution, and audit logging.
Default stack:
- Bannerlord C# module: `LocalDiplomacy`
- External service: Python 3.11+ FastAPI app, `LocalDiplomacy.Agent`
- LLM backend: KoboldCpp OpenAI-compatible `/v1/chat/completions`, launched by the agent from the app working directory
- Local memory: Mem0 OSS with Qdrant
- Audit/source-of-truth event log: SQLite
- Tool calling: OpenAI-style tools, with mod-side validation before any Bannerlord mutation
## Goals
- Replace giant prompt text files with compact, structured game-state packets.
- Keep rich NPC dialogue, world events, and diplomatic statements.
- Allow AI to propose actions, but never let AI directly mutate the game.
- Make the AI layer swappable and debuggable outside Bannerlord.
- Store long-term character, kingdom, and campaign memories locally.
- Keep all player and campaign data on the user's machine by default.
## Non-Goals For v1
- Do not copy AIInfluence source, binary code, or assets.
- Do not implement intimate/child systems in the first pass.
- Do not implement TTS, STT, lip-sync, or generated voice in the first pass.
- Do not require cloud APIs.
- Do not send full save files, huge world dumps, or unbounded conversation logs to the model.
## Repository Layout
Target layout:
```text
LocalDiplomacy/
├── docs/
│ └── LocalDiplomacy_PLAN.md
├── src/
│ ├── LocalDiplomacy/
│ │ ├── LocalDiplomacy.csproj
│ │ ├── SubModule.cs
│ │ ├── Agent/
│ │ ├── Campaign/
│ │ ├── Contracts/
│ │ ├── Diplomacy/
│ │ ├── Settings/
│ │ └── UI/
│ └── LocalDiplomacy.Agent/
│ ├── pyproject.toml
│ ├── config.example.yaml
│ ├── localdiplomacy_agent/
│ │ ├── app.py
│ │ ├── config.py
│ │ ├── contracts.py
│ │ ├── koboldcpp_client.py
│ │ ├── memory.py
│ │ ├── prompts.py
│ │ ├── tools.py
│ │ └── event_log.py
│ └── tests/
└── module/
└── LocalDiplomacy/
├── SubModule.xml
├── bin/
├── GUI/
└── ModuleData/
```
## Bannerlord Module Design
The Bannerlord module should stay intentionally small and defensive.
Responsibilities:
- Load as a singleplayer module named `LocalDiplomacy`.
- Register campaign behavior for dialogue and world tick hooks.
- Collect relevant context from Bannerlord APIs.
- Serialize context into compact JSON requests.
- Send requests to the local agent.
- Render NPC replies, world events, and diplomatic statements.
- Validate proposed actions against current game state.
- Execute approved and valid actions.
- Report action results back to the agent.
Suggested dependencies:
- `Bannerlord.Harmony`
- `Bannerlord.UIExtenderEx`
- Native Bannerlord modules: `Native`, `SandBoxCore`, `Sandbox`, `StoryMode`, `CustomBattle`
- Optional later: MCM for settings UI
Core C# subsystems:
- `AgentClient`: HTTP client for the FastAPI service.
- `ContextBuilder`: creates compact request DTOs from Bannerlord state.
- `ActionValidator`: checks proposed actions against whitelists and live state.
- `ActionExecutor`: mutates Bannerlord only after validation.
- `DialogueBehavior`: hooks conversation entry points.
- `WorldTickBehavior`: sends periodic compact world diffs.
- `LocalDiplomacySettings`: service URL, timeout, debug flags, confirmation policy.
- `DebugLog`: mod-local logs for requests, responses, validation failures, and connectivity.
Feature coverage from `reference/aiinfulence_features.md` must be represented in the Bannerlord connector as validated action/context categories, not as in-game AI logic. The connector should expose live state and validate proposals for AI dialogue, NPC memory, trust/lie reactions, dynamic world events, diplomacy, romance/intimacy, settlement combat, death history, party tasks, trade/workshops/items, naval context, recruitment opportunities, visit history, non-combatant protection, and economic effects.
## External Agent Design
`LocalDiplomacy.Agent` is a local FastAPI app listening on `127.0.0.1:8766`.
Responsibilities:
- Accept compact game-state requests from the mod.
- Locate and launch `koboldcpp.exe` and `model.gguf` from the directory the app is run from.
- Monitor the KoboldCpp child process and wait for its API to become reachable before accepting AI requests.
- Retrieve relevant memories from Mem0/Qdrant.
- Read recent events from SQLite.
- Build bounded prompts/messages.
- Call KoboldCpp via OpenAI-compatible chat completions.
- Provide tool schemas to KoboldCpp.
- Execute local read/write-intention tools.
- Return final assistant text and action proposals to the mod.
- Persist memory writes and audit events.
Default command:
```powershell
uvicorn localdiplomacy_agent.app:app --host 127.0.0.1 --port 8766
```
Expected runtime files in the app working directory:
```text
LocalDiplomacy.Agent/
├── koboldcpp.exe
├── model.gguf
├── config.yaml
└── data/
```
On startup, the agent should:
1. Resolve the current working directory.
2. Verify `koboldcpp.exe` exists there.
3. Verify `model.gguf` exists there.
4. Start KoboldCpp as a child process if no compatible server is already reachable at the configured URL.
5. Launch with the configured port, context size, GPU flags, and `--jinjatools`.
6. Poll `/api/extra/version` or `/v1/models` until ready.
7. Surface a clear `/health` error if the executable, model, or API startup fails.
Default config:
```yaml
server:
host: "127.0.0.1"
port: 8766
koboldcpp:
autostart: true
executable_path: "./koboldcpp.exe"
model_path: "./model.gguf"
base_url: "http://127.0.0.1:5001"
chat_path: "/v1/chat/completions"
model: "local-model"
port: 5001
context_size: 8192
extra_args:
- "--jinjatools"
timeout_seconds: 120
tool_mode: "openai_tools"
json_repair_retry: true
memory:
provider: "mem0"
vector_store: "qdrant"
qdrant_host: "127.0.0.1"
qdrant_port: 6333
collection: "localdiplomacy_memories"
embedder_provider: "ollama"
embedder_model: "nomic-embed-text"
llm_provider: "openai_compatible"
event_log:
sqlite_path: "./data/localdiplomacy_events.sqlite3"
generation:
temperature: 0.7
max_tokens: 800
```
## KoboldCpp Integration
Use KoboldCpp latest stable. For packaged/local use, place `koboldcpp.exe` and the selected GGUF model named exactly `model.gguf` in the directory where `LocalDiplomacy.Agent` is run. The agent should launch KoboldCpp automatically using those files.
Default agent-launched command:
```powershell
.\koboldcpp.exe --model .\model.gguf --port 5001 --contextsize 8192 --jinjatools
```
Manual launch remains useful for debugging:
```powershell
koboldcpp.exe --model C:\Models\your-model.gguf --port 5001 --contextsize 8192 --jinjatools
```
Use `--jinja` or a custom chat template only when `model.gguf` requires it. Some modern instruct models are template-sensitive, so every model should pass a smoke test before gameplay.
Agent request shape:
```json
{
"model": "local-model",
"messages": [
{ "role": "system", "content": "LocalDiplomacy system instructions..." },
{ "role": "user", "content": "Compact scene packet..." }
],
"tools": [],
"tool_choice": "auto",
"temperature": 0.7,
"max_tokens": 800
}
```
Tool loop:
1. Agent sends messages plus tool schemas to KoboldCpp.
2. If the assistant returns `tool_calls`, the agent executes those tool handlers locally.
3. Agent appends `role: tool` messages with results.
4. Agent calls KoboldCpp again until the assistant returns final text or a configured loop limit is reached.
5. Agent returns the final response plus queued action proposals to the mod.
Fallback mode:
- If a model fails native tool calling, use a strict JSON response contract.
- Validate the JSON against Pydantic models.
- Attempt one repair prompt.
- If still invalid, return a graceful failure response and no actions.
## Public HTTP Interfaces
### `GET /health`
Returns service and dependency status.
Response:
```json
{
"status": "ok",
"agent_version": "0.1.0",
"koboldcpp": "reachable",
"memory": "reachable",
"event_log": "ok"
}
```
### `POST /conversation/respond`
Main dialogue endpoint.
Request:
```json
{
"campaign_id": "campaign-123",
"save_id": "save-abc",
"turn_id": "turn-001",
"player_message": "What do you think of the war?",
"player": {
"id": "player",
"name": "Aldric",
"clan_id": "player_clan",
"kingdom_id": "vlandia",
"gold": 12000,
"traits": {}
},
"npc": {
"id": "lord_derthert",
"name": "Derthert",
"occupation": "lord",
"clan_id": "dey_meroc",
"kingdom_id": "vlandia",
"traits": {},
"relation_to_player": 12
},
"scene": {
"location_id": "town_sargot",
"conversation_state": "lord_dialogue",
"player_is_prisoner": false,
"npc_is_prisoner": false
},
"nearby_parties": [],
"nearby_settlements": [],
"kingdom_state": {},
"recent_events": []
}
```
Response:
```json
{
"turn_id": "turn-001",
"assistant_text": "The war has teeth, but not yet a mind...",
"mood": "wary",
"tone": "formal",
"visible_world_events": [],
"game_actions": [],
"memory_writes": [],
"warnings": []
}
```
### `POST /world/tick`
Receives compact world diffs. The mod should call this on a low-frequency campaign tick, not every frame.
Request:
```json
{
"campaign_id": "campaign-123",
"save_id": "save-abc",
"campaign_day": 481.5,
"diff_id": "diff-001",
"changed_kingdoms": [],
"changed_settlements": [],
"wars": [],
"peace_deals": [],
"battles": [],
"notable_relation_changes": []
}
```
Response:
```json
{
"diff_id": "diff-001",
"accepted": true,
"created_events": [],
"warnings": []
}
```
### `POST /actions/result`
Reports whether the mod executed, rejected, or failed an action proposal.
Request:
```json
{
"campaign_id": "campaign-123",
"turn_id": "turn-001",
"action_id": "action-001",
"status": "executed",
"reason": null,
"result_summary": "Derthert proposed peace to Battania."
}
```
Response:
```json
{
"accepted": true
}
```
### `GET /debug/status`
Returns runtime diagnostics for the debug UI.
Response:
```json
{
"last_koboldcpp_latency_ms": 5420,
"memory_count_estimate": 128,
"queued_action_count": 0,
"recent_errors": []
}
```
## Core Contracts
### `GameAction`
All AI action proposals use one normalized envelope:
```json
{
"action_id": "action-001",
"action_type": "propose_peace",
"actor_id": "lord_derthert",
"target_id": "kingdom_battania",
"args": {
"tribute_daily": 500
},
"confidence": 0.82,
"reason": "Vlandia is losing two fronts and needs time.",
"requires_player_confirmation": true
}
```
Validation rules:
- `action_type` must be in the whitelist.
- `actor_id` and `target_id` must resolve in current game state where required.
- Dangerous actions always require confirmation unless explicitly disabled in settings.
- The executor must reject stale proposals if game state changed materially.
- The AI's `reason` is for logs/UI only and must not override validation.
### Initial Action Whitelist
Dialogue:
- `attack`
- `surrender`
- `accept_surrender`
- `release`
- `propose_marriage`
- `accept_marriage`
- `reject_marriage`
Party:
- `follow_player`
- `go_to_settlement`
- `return_to_player`
- `attack_party`
- `patrol_settlement`
- `wait_near_settlement`
- `siege_settlement`
- `raid_village`
Diplomacy:
- `declare_war`
- `propose_peace`
- `accept_peace`
- `reject_peace`
- `propose_alliance`
- `accept_alliance`
- `reject_alliance`
- `break_alliance`
- `propose_trade_agreement`
- `accept_trade_agreement`
- `reject_trade_agreement`
- `end_trade_agreement`
- `demand_territory`
- `transfer_territory`
- `demand_tribute`
- `accept_tribute`
- `reject_tribute`
- `demand_reparations`
- `accept_reparations`
- `reject_reparations`
Recruitment:
- `hire_mercenary`
- `dismiss_mercenary`
- `offer_vassalage`
- `dismiss_vassal`
- `join_player_clan`
- `join_player_kingdom`
- `hire_mercenary_clan`
- `kick_from_clan`
- `dismiss_npc_mercenary`
- `release_npc_vassal`
## Tool Handlers
Tools exposed to the LLM should be narrow and descriptive.
Read-only tools:
- `get_npc_profile(character_id)`
- `get_relation_summary(source_id, target_id)`
- `get_nearby_parties(reference_id, limit)`
- `get_kingdom_status(kingdom_id)`
- `get_settlement_status(settlement_id)`
- `get_military_info(scope_id)`
- `get_naval_info(scope_id)`
- `get_trade_info(scope_id)`
- `get_romance_status(character_id, player_id)`
- `get_death_history(character_id)`
- `get_visit_history(character_id)`
- `get_recruitment_opportunities(scope_id)`
- `search_memory(query, campaign_id, character_id, kingdom_id, limit)`
- `list_recent_events(campaign_id, scope_id, limit)`
Write/intention tools:
- `remember_fact(scope, text, importance, tags)`
- `analyze_lie(speaker_id, listener_id, claim)`
- `propose_game_action(action_type, actor_id, target_id, args, confidence, reason)`
- `publish_diplomatic_statement(kingdom_id, speaker_id, statement_text, related_event_id)`
- `create_world_event(event_type, title, summary, involved_ids, importance, expires_after_days)`
- `update_world_event(event_id, summary, spread_to_ids, importance_delta)`
- `record_war_statistics(war_id, summary, stats)`
- `record_death_history(character_id, history_text, interaction_count)`
Important rule: no tool handler directly mutates Bannerlord. Mutation tools queue proposals and return proposal IDs. The mod decides whether execution is legal.
The initial action catalog should include every feature family in the AIInfluence reference: personality/backstory/speech generation, trust and lie detection, conversation memory, known secrets/info, dynamic event creation/spread/update/personalization, diplomatic statements/rounds/alliances/trade/territory/tribute/reparations/war fatigue/war stats/clan expulsion/pardons, romance/intimacy/marriage/degradation, AI-managed settlement combat, death history, party task chains, workshop sales, item exchanges, RP item creation, recruitment detection, settlement/party/visit tracking, non-combatant protection, naval context, and economic effects.
## Memory System
Use Mem0 OSS with Qdrant by default.
Memory scopes:
- `campaign_id`: isolates saves/campaigns.
- `character_id`: NPC-specific memory.
- `kingdom_id`: ruler and diplomacy memory.
- `player_id`: player-facing preferences and promises.
Memory categories:
- `personal_fact`
- `relationship_shift`
- `promise`
- `grudge`
- `secret`
- `diplomatic_history`
- `world_event_summary`
- `player_preference`
Retrieval policy:
- Retrieve by campaign plus the active NPC/kingdom.
- Include only the top relevant memories.
- Prefer atomic facts over full conversation excerpts.
- Include timestamps and confidence where available.
- Never rely on Mem0 as the authoritative game state; live state comes from the mod.
SQLite event ledger:
- Append every request summary, response summary, tool call, action proposal, action result, and memory write.
- Store enough metadata to debug behavior without storing huge raw prompts by default.
- Allow debug mode to store full prompts/responses for troubleshooting.
## Prompt Strategy
System prompt should define:
- The assistant role: an in-world Bannerlord NPC/diplomacy narrator.
- The hard boundary: never invent IDs, never claim actions happened unless tools/results confirm them.
- Tool usage rules: use read tools for missing relevant facts, use proposal tools for intended actions.
- Safety rules: major game actions require explicit agreement and valid targets.
- Style: grounded medieval political roleplay, concise enough for live gameplay.
Per-turn context should include:
- Current speaker and listener summaries.
- Live scene state.
- Relevant nearby parties/settlements.
- Current kingdom status if politically relevant.
- Retrieved memories.
- Recent events from SQLite.
- Player message.
Prompt budget policy:
- Keep the system message stable.
- Keep live state compact.
- Keep memories capped.
- Summarize old events.
- Do not paste giant rule files or full campaign state.
## UI Plan
v1 UI should mirror the useful feel of AIInfluence without copying assets:
- Dialogue popup/window for AI conversation.
- World events button and window.
- Diplomatic statements list.
- Debug status panel.
- Connection error banner when the external service is offline.
UI states:
- `Idle`
- `WaitingForAgent`
- `ShowingResponse`
- `ActionRequiresConfirmation`
- `AgentUnavailable`
- `ValidationFailed`
## Settings
Bannerlord settings:
- Agent URL, default `http://127.0.0.1:8766`
- Request timeout
- Enable debug logging
- Store full prompts in debug logs
- Auto-execute safe actions
- Always confirm diplomacy actions
- Always confirm hostile actions
- World tick interval
Agent settings:
- KoboldCpp autostart enabled/disabled
- KoboldCpp executable path, default `./koboldcpp.exe`
- KoboldCpp model path, default `./model.gguf`
- KoboldCpp base URL
- KoboldCpp port, context size, and extra launch args
- Model name
- Tool-call mode
- Temperature and max tokens
- Memory provider settings
- Qdrant connection
- SQLite path
- Prompt/debug logging level
## Implementation Milestones
### Milestone 1: Documentation and Contracts
- Add this architecture plan.
- Define JSON schemas/Pydantic models for all public contracts.
- Define matching C# DTOs.
- Add sample request/response fixtures.
### Milestone 2: External Agent Skeleton
- Create FastAPI app.
- Implement `/health`, `/debug/status`, and config loading.
- Implement KoboldCpp runtime file checks for `koboldcpp.exe` and `model.gguf`.
- Implement child-process launch/stop management for KoboldCpp.
- Add event log initialization.
- Add unit tests for config and contracts.
### Milestone 3: KoboldCpp Chat Loop
- Implement OpenAI-compatible client.
- Wait for autostarted KoboldCpp readiness before sending chat requests.
- Implement tool schema generation.
- Implement bounded tool loop.
- Add smoke test script for KoboldCpp connectivity and tool-call behavior.
### Milestone 4: Memory Layer
- Add Mem0/Qdrant integration.
- Add scoped memory writes and searches.
- Add fallback no-memory mode when Qdrant is offline.
- Test memory isolation by campaign and character.
### Milestone 5: Bannerlord Thin Client
- Create C# module skeleton and `SubModule.xml`.
- Add service health check.
- Add compact context builder.
- Add dialogue request/response path.
- Add graceful offline behavior.
### Milestone 6: Action Proposal and Validation
- Implement action proposal contract end-to-end.
- Add mod-side action whitelist.
- Validate IDs and game-state preconditions.
- Execute one safe action category first, then expand.
### Milestone 7: Diplomacy and Events
- Add diplomatic statement creation.
- Add world event ingestion and display.
- Add ruler dialogue actions.
- Add confirmation UI for dangerous actions.
## Test Plan
Agent unit tests:
- Contract validation rejects malformed requests.
- Tool schemas match handler signatures.
- `GameAction` normalization fills required defaults.
- Memory scope keys isolate campaigns and NPCs.
- SQLite event ledger appends expected records.
Agent integration tests:
- `/health` succeeds with dependencies mocked.
- KoboldCpp chat succeeds with a basic response.
- Tool loop handles one read-only tool call.
- Tool loop handles one action proposal.
- Invalid tool JSON triggers one repair attempt.
- Failed repair returns no actions.
Bannerlord smoke tests:
- Module loads with required dependencies.
- Settings load defaults.
- Agent unavailable state is shown gracefully.
- Dialogue UI sends a compact request.
- Response displays without executing actions.
- Invalid action proposal is rejected and logged.
Gameplay acceptance tests:
- NPC remembers a prior promise after save reload.
- Ruler creates a diplomatic statement from recent events.
- AI refuses or asks clarification when a target id is missing.
- Peace/war proposals require confirmation.
- Large campaign state remains compact and avoids giant generated text files.
## Rollout Strategy
Start with developer-only debug mode:
1. Put `koboldcpp.exe` and `model.gguf` beside the agent startup directory.
2. Run Qdrant manually or through Docker.
3. Run `LocalDiplomacy.Agent`; it should launch KoboldCpp automatically.
4. Launch Bannerlord with the mod.
5. Use debug panel to verify service health.
6. Test one dialogue response with actions disabled.
7. Enable action proposals but keep all execution behind confirmation.
8. Gradually enable safe auto-execution for low-risk actions.
## Risks And Mitigations
- KoboldCpp tool calling varies by model.
- Mitigation: require a smoke test and keep JSON fallback mode.
- KoboldCpp executable/model may be missing from the working directory.
- Mitigation: startup checks must produce explicit errors naming the missing file and expected path.
- Autostarted KoboldCpp may hang during model load.
- Mitigation: readiness polling, startup timeout, process logs, and a manual-launch override.
- AI may hallucinate entities or action parameters.
- Mitigation: IDs must come from context/tools and mod-side validation is mandatory.
- Memory retrieval may surface stale or wrong facts.
- Mitigation: live game state always wins; memory is contextual, not authoritative.
- External service may be offline or slow.
- Mitigation: short health checks, clear UI state, configurable timeouts.
- Bannerlord action APIs can have hidden constraints.
- Mitigation: implement action categories incrementally and log validation failures.
## Research Sources
- KoboldCpp README and wiki: https://github.com/LostRuins/koboldcpp and https://github.com/LostRuins/koboldcpp/wiki
- KoboldCpp releases/tool-calling notes: https://github.com/LostRuins/koboldcpp/releases
- Mem0 OSS docs: https://github.com/mem0ai/mem0/blob/main/LLM.md
- Mem0/OpenMemory local MCP overview: https://mem0.ai/blog/how-to-make-your-clients-more-context-aware-with-openmemory-mcp
- Qdrant Mem0 integration: https://qdrant.tech/documentation/frameworks/mem0/
- Graphiti/Zep comparison option: https://www.getzep.com/product/open-source/