Interaction Layer – Cloud Configuration (nadine_phd)¶
The Hybrid Cloud version (nadine_phd) runs the same LangGraph interaction layer as nadine_local, but moves the agents that matter most for reliability to hosted models and replaces the front of the graph with one structured call. This page documents what changed and how to operate it. The graph itself, the agents, and the memory system are described on the Overview, Agents & Graph, and Memory & RAG pages; the reasons for the split are on the Platforms page.
Agent-to-Model Mapping¶
interaction/config.yaml maps each agent to an LLM profile. The mapping is the same file in both pipeline modes; the mode decides which agents run.
| Agent | Profile | Backend / model | Where | Pipeline mode |
|---|---|---|---|---|
turn_understanding |
understand_gpt |
OpenAI gpt-5.4-mini |
cloud | merged only |
response_agent |
response_gpt |
OpenAI gpt-5.4-mini |
cloud | both |
memory_update_agent |
gpt_json |
OpenAI gpt-5.4-mini |
cloud | both |
search_router |
gpt_json |
OpenAI gpt-5.4-mini |
cloud | both |
vision_router |
gpt_json |
OpenAI gpt-5.4-mini |
cloud | both |
vision_description |
vision_llm |
Ollama qwen2.5vl:3b |
local | both |
intention_classifier |
ft_intent_classifier |
Ollama nadine-intent_classifier |
local | legacy only |
affective_appraisal |
ft_affective_appraisal |
Ollama nadine-affective_appraisal |
local | legacy only |
orchestration_agent |
ft_orchestration_agent |
Ollama nadine-orchestration_agent |
local | legacy only |
contextualizer |
ft_episodic_memory |
Ollama nadine-episodic_memory |
local | legacy only |
In merged mode the four local fine-tuned agents are never invoked: the understand node produces their outputs in one call, and the contextualizer is only called from inside the legacy orchestrator. Text memory retrieval and storage use no LLM in either mode; they embed with the local ONNXMiniLM_L6_V2 function as in nadine_local.
The config comments record why each cloud move was made:
memory_update_agent(wasft_memory_update): the fine-tuned model "reproduces its training behavior and ignores the hardened extraction rules — it recorded 'I was supervising you in Singapore' as current job/location. Extraction and episodic encoding run at most once per conversation, so cloud cost is negligible."search_routerandvision_router(weresmall_llm): they "only run on search/vision turns (which already pay a web/camera round trip), and the cloud model is far more reliable at instruction-following. Frees router VRAM on GPU 1."vision_description"stays LOCAL deliberately: switching tovision_gptwould send camera frames of participants to OpenAI — a study-privacy decision, not a latency one."
LLM Profiles¶
Backend resolution¶
load_agent_llm(agent_name) in nadine/agents/utils.py resolves the profile in this order:
NADINE_PROFILE_<agent_name>environment variable, if set (for exampleNADINE_PROFILE_response_agent=claude_responsepoints one agent at Claude for an A/B run without editing the config).interaction.agents.<agent_name>inconfig.yaml.- The agent's built-in default (
gpt_jsonforturn_understanding,response_llmforresponse_agent,big_llmotherwise).
The profile's backend key selects the client: openai, anthropic, vllm, or, when absent, ollama. Instances are cached per effective parameter set, so one HTTP connection and one model handle are reused across turns.
Cloud profiles¶
| Profile | Backend | Model | temperature | max_tokens | timeout | JSON |
|---|---|---|---|---|---|---|
understand_gpt |
openai | gpt-5.4-mini |
0.0 | 300 | 2.5 s | yes |
response_gpt |
openai | gpt-5.4-mini |
0.3 | 160 | 15.0 s | no |
gpt_json |
openai | gpt-5.4-mini |
0.0 | 300 | 2.5 s | yes |
gpt_text |
openai | gpt-5.4-mini |
0.0 | 120 | 2.5 s | no |
vision_gpt |
openai | gpt-5.4-mini |
0.0 | 150 | 2.5 s | no |
claude_understand |
anthropic | claude-haiku-4-5 |
0.0 | 300 | 2.5 s | prompt-only |
claude_response |
anthropic | claude-haiku-4-5 |
0.3 | 500 | 15.0 s | no |
gpt4o_mini |
openai | gpt-4o-mini |
0.0 | 100 | none | no |
gpt4o_mini_long |
openai | gpt-4o-mini |
0.3 | 500 | none | no |
vision_gpt, the two Claude profiles, and the two gpt-4o-mini profiles are defined but not mapped to any agent by default.
Profile keys and their effect:
timeoutis the per-call limit in seconds;max_retries(default 2) re-issues a call that hits it. The loader comment states the purpose: "a call stuck in the server-side slow tail is aborted attimeoutand retried (the retry almost always lands on the fast path). Tunetimeoutabove p95 (~2.2s) so it never fires on healthy calls." The 2.5 s value on the per-turn calls follows that rule; the response agent gets 15 s because it streams a full reply.format: jsonon an OpenAI profile setsresponse_format: {"type": "json_object"}. Claude has no equivalent, soclaude_understandrelies on the prompt's JSON-only instruction and onJsonOutputParserstripping any markdown fences.max_tokensreplaces the Ollamanum_predict;num_ctxandrepeat_penaltyare ignored by cloud backends.
Connection handling¶
- All OpenAI instances share one
httpx.Clientwith 10 keep-alive connections, a 300 s keep-alive expiry, a 60 s request timeout, and a 10 s connect timeout. DialogueManagercallsstart_cloud_keepalive()at startup. If any mapped profile uses a cloud backend, a daemon thread sends a one-token request per provider every 60 s so the first turn after an idle gap does not pay a reconnect.- The Anthropic client is imported lazily, so
langchain_anthropicis only needed when a Claude profile is mapped or selected through the environment override.
Merged Turn-Understanding Pipeline¶
Mode selection¶
interaction.pipeline.mode in config.yaml is merged. The NADINE_PIPELINE_MODE environment variable overrides it (merged or legacy); when neither is set the code defaults to legacy. The mode is read once when the graph is built.
The understand node¶
turn_understanding.py defines a Pydantic schema, TurnUnderstanding, that one call must fill:
intent– one offirst_greeting,update_user_info,end_conversation,language_change,continue_conversation.emotion– Nadine's own appraised reaction,{label, intensity}with the seven labelshappy,sad,angry,fearful,disgusted,surprised,neutraland intensity 0.0–1.0. This drives tone and the PAD state.user_emotion– the user's currently felt emotion in the same form. This drives selective scene storage in perception and is rated only for emotions felt now, not ones recalled or mentioned.is_recall– true when the user asks Nadine to remember anything from the past or earlier in the conversation. This gates text-memory injection and the permissive visual-recall threshold.plan– a list of{agent, message}steps oversearch_agent,knowledge_rag_agent,vision_agent,response_agent.
The prompt carries the routing rules that the fine-tuned orchestrator learned from data, in text: real-time facts go to search, questions about Nadine herself and her own past experiences go to the knowledge agent, anything visual goes to vision, and recall of the user's own information or shared moments goes straight to the response agent because the profile and any matching scene are already loaded. The chat history is passed as plain text in the same call, which is why the contextualizer is not needed.
Guards around the call¶
understand() in graph.py wraps the call in the same deterministic checks the three legacy nodes applied, plus a few added for the study:
- A pending name confirmation short-circuits to
update_user_infobefore any model call. - On any exception the node falls back to
continue_conversation, neutral emotion, and a singleresponse_agentstep. - Unknown intents become
continue_conversation. The legacy regex overrides still apply: a name introduction upgradesfirst_greetingtoupdate_user_info; a question about Nadine downgradesupdate_user_info;end_conversationneeds an explicit farewell word. One addition: an unambiguous farewell that is not a question is upgraded toend_conversationregardless of the model output, because that intent gates the episodic save, the language reset, and the wave. language_changesets the language and is then treated ascontinue_conversation.first_greetingis accepted once per user.- The emotion label is converted with the same
emotion_to_padlookup andupdate_affect_stateas the legacy appraisal node. - The plan is filtered to known agents; greetings and goodbyes always get a single
response_agentstep.
After the appraisal the node publishes nadine/affect/state. Compared with nadine_local the payload carries four more fields: user_label, user_intensity, user_message (the current utterance, truncated to 500 characters, used as the scene anchor), and suppress_storage (true on recall turns and goodbyes, so those turns do not create scenes).
Routing after the call¶
_route_by_plan() contains no model call. It sends the turn to the first planned sub-agent, or to response_agent when the plan is empty or a name confirmation is pending. The merged wiring is:
START → understand
├─ intent == update_user_info → memory_update_agent → (response_agent | END | memory_retrieve_agent)
└─ otherwise → memory_retrieve_agent
memory_retrieve_agent → [search_agent | vision_agent | knowledge_rag_agent]* → response_agent
response_agent → affective_update → (intent == end_conversation → memory_update_agent | END)
Each sub-agent pops its step and re-routes on the remaining plan, as in the legacy graph. affective_update still runs after the response but only keeps the state that the appraisal already set.
What it changed¶
Per turn, three sequential local calls (intent, appraisal, orchestration) and the contextualizer are replaced by one cloud call bounded at 2.5 s. The design note in turn_understanding.py states the premise: the three tasks "are independent functions of (user message, history), so a single capable model can produce all three at once." The module was first scored standalone against the existing intent, routing, and emotion sample sets before the graph was rewired.
Memory Extraction and Knowledge RAG¶
Profile extraction rules¶
memory_update_agent.py runs on gpt_json. Its extraction prompt is stricter than the version the local adapter was trained on:
- Only facts stated as currently true about the user are extracted; past-tense statements ("I worked at…", "I used to live in…") are memories, not profile fields.
- Statements about the user's shared history with Nadine ("I was supervising you", "your face was modeled on mine") never fill
current_company,current_position, orlocation. locationmeans where the user lives now, not places mentioned or visited.
The profile schema (user_name, current_company, current_position, location, hobbies, interests) and the identity-conflict guard are unchanged from nadine_local; only the rules and the model changed.
Episodic encoding¶
The episodic summary prompt now requires the model to preserve the user's concrete claims (names, relationships, biographical and shared-history statements) in the observation and result fields, because generic summaries were dropping exactly the details that later recall depends on. The loader requests 220 output tokens for this call; that override only applies to the Ollama backend, so on gpt_json the profile's max_tokens: 300 is the effective limit.
Knowledge RAG¶
knowledge_RAG_agent.py embeds with OpenAI text-embedding-3-large instead of the local nomic-embed-text model. The code comment gives the reason: "markedly better ranking than the local nomic-embed-text, and consistent with the visual memory description channel which already uses this model." Two further changes:
- Documents are split with
MarkdownHeaderTextSplitteron both#and##, and each chunk is prefixed with its header path. Splitting on##only had glued top-level headers onto the end of the previous chunk. - The retriever returns
k=3chunks instead of one, because Nadine's experiences span several sections and the single nearest chunk was often a related but wrong one.
The index under db/knowledge/chroma is specific to the embedding model. After changing the model, delete that directory so it is rebuilt from the markdown files in db/knowledge/rag_files.
Running This Configuration¶
Keys and environment¶
interaction/.env must contain OPENAI_API_KEY; the default mapping does not start without it. Other entries:
ANTHROPIC_API_KEY– only for the Claude profiles.GOOGLE_APPLICATION_CREDENTIALS,GOOGLE_CLOUD_PROJECT,SERPER_API_KEY– unchanged fromnadine_local.LANGCHAIN_TRACING_V2/LANGCHAIN_API_KEY– present but tracing was disabled after the monthly trace quota was exhausted. In study mode every prompt and completion is logged locally bynadine/common/prompt_logger.pyinstead.
The conda environment pins openai==2.15.0 and anthropic==0.76.0.
Local models still required¶
Ollama still serves qwen2.5vl:3b for vision description and, in legacy mode, the fine-tuned nadine-* models. start_nadine.sh pre-warms only nadine-memory_update and qwen2.5:1.5b-instruct in Step 1 and qwen2.5vl:3b in Step 3 (skipped with --no-vision-agent); mistral-small3.2 is no longer warmed because the response agent is on the cloud. The script's own comment notes that the fine-tuned classifiers "are only used by the LEGACY fallback" and "warm on first use there." The two models it does pre-warm are not used by the default mapping either, so on the default path the only local model that matters at runtime is the vision model.
Start script differences¶
Relative to nadine_local, the nadine_phd launcher enables the vision and memory agents by default and adds --no-visual-memory, --study, and --zoom-audio. Those flags concern the study and the audio path rather than the cloud configuration and are listed on the Usage page.
Language¶
The default language is English. interaction.runtime.current_language in config.yaml only seeds the value; the running language is persisted in db/runtime_language.json by language_config.py, and the config file is no longer rewritten at runtime.
Known Constraints¶
- Network dependency. Turn understanding, the response, memory extraction, and both routers need the OpenAI API. Each call is bounded by its profile timeout and retried twice; if the API is unreachable,
understandfalls back to a neutralcontinue_conversationturn routed to the response agent, which then fails in the same way. - Hosted model drift. The behavior of every cloud agent is tied to the hosted
gpt-5.4-miniversion, which can change outside the repository. - Data leaving the machine. User speech text, the conversation history passed as context, extracted profile facts, episodic summaries, and knowledge-base chunks are sent to OpenAI. Camera frames, the vision description, face embeddings, and the text-memory embeddings stay local.
- Two embedding models. Knowledge RAG uses OpenAI embeddings while user text memory keeps the local ONNX MiniLM model; the two indexes cannot be queried with each other's vectors.
- Legacy mode still needs the adapters. Switching to
NADINE_PIPELINE_MODE=legacyrequires allnadine-*Ollama models to be present and loads them on first use rather than at startup.