Mercurial
diff mrjunejune/conversation_api.c @ 265:056790c4fb0d
add role-aware Epi assistant prompts
Add verified June knowledge, guest/member/admin Copilot profiles, profile-isolated session recovery, animated Epi greetings, and a single authoritative runtime config workflow for inference.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 10:50:30 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line diff
--- a/mrjunejune/conversation_api.c Fri Aug 07 07:34:12 2026 -0700 +++ b/mrjunejune/conversation_api.c Fri Aug 07 10:50:30 2026 -0700 @@ -22,6 +22,8 @@ #define CONVERSATION_EVENT_NAME_MAX 64 #define CONVERSATION_ACTIVE_MAX 4 #define CONVERSATION_TURNS_PER_MINUTE 60 +#define CONVERSATION_PROMPT_VERSION 1 +#define CONVERSATION_KNOWLEDGE_VERSION 1 /* Buffer large enough for a guest Set-Cookie directive */ #define CONV_GUEST_COOKIE_CAPACITY (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256) @@ -382,6 +384,32 @@ } } +static boolean Conversation_API_Prompt_Profile_From_Principal( + const Auth_Principal *p_principal, + Inference_Prompt_Profile *p_profile) +{ + if (!p_principal || !p_profile) + return FALSE; + if (p_principal->kind == AUTH_PRINCIPAL_GUEST) + { + *p_profile = INFERENCE_PROMPT_PROFILE_PUBLIC_VISITOR; + return TRUE; + } + if (p_principal->kind != AUTH_PRINCIPAL_USER) + return FALSE; + if (strcmp(p_principal->role, "member") == 0) + { + *p_profile = INFERENCE_PROMPT_PROFILE_INVITED_FRIEND; + return TRUE; + } + if (strcmp(p_principal->role, "admin") == 0) + { + *p_profile = INFERENCE_PROMPT_PROFILE_JUNE_ADMIN; + return TRUE; + } + return FALSE; +} + static void Conversation_API_Send_Stream_Error( Seobeo_Handle *p_handle, int status, @@ -1515,6 +1543,14 @@ p_handle, 500, "internal_error", "Session error"); return; } + Inference_Prompt_Profile prompt_profile; + if (!Conversation_API_Prompt_Profile_From_Principal( + &principal, &prompt_profile)) + { + Conversation_API_Send_Stream_Error( + p_handle, 403, "invalid_role", "Conversation role is not supported"); + return; + } if (!found) { Conversation_API_Send_Stream_Error( @@ -1605,6 +1641,72 @@ return; } + /* --- Load owner-verified conversation record for transcript history --- */ + /* Done before quota reservation: if the conversation is missing or + * not owned by this principal we fail fast with no quota side-effects. */ + Conversation_Record hist_record; + memset(&hist_record, 0, sizeof(hist_record)); + { + Conversation_Store_Result hist_result = Conversation_Store_Get_Owned( + g_conversation_store, conversation_id, &owner, &hist_record, p_arena); + if (hist_result == CONVERSATION_STORE_NOT_FOUND) + { + Conversation_API_Release_Turn_Slot(); + Conversation_API_Send_Stream_Error( + p_handle, 404, "not_found", "Conversation not found"); + return; + } + if (hist_result != CONVERSATION_STORE_OK) + { + Conversation_API_Release_Turn_Slot(); + Conversation_API_Send_Stream_Error( + p_handle, 500, "history_load_failed", "Unable to load conversation"); + return; + } + } + + /* Build bounded history: last ≤20 qualifying turns from the persisted + * record. Include non-empty user turns and completed, non-empty assistant + * turns only. The new prompt is not yet part of history. */ + Inference_Bridge_History_Message hist_msgs[INFERENCE_BRIDGE_HISTORY_MAX]; + uint32 hist_count = 0; + { + /* Collect qualifying turn indices in a sliding window of HIST_MAX. */ + size_t qidx[INFERENCE_BRIDGE_HISTORY_MAX]; + uint32 qcount = 0; + size_t num_turns = Dowa_Array_Length(hist_record.turns); + for (size_t ti = 0; ti < num_turns; ti++) + { + Conversation_Turn *t = &hist_record.turns[ti]; + if (!t->role || !t->content || !t->status) + continue; + boolean is_user = strcmp(t->role, "user") == 0; + boolean is_asst = strcmp(t->role, "assistant") == 0; + if (!is_user && !is_asst) + continue; + if (t->content[0] == '\0') + continue; + if (is_asst && strcmp(t->status, "complete") != 0) + continue; + if (qcount < INFERENCE_BRIDGE_HISTORY_MAX) + qidx[qcount++] = ti; + else + { + /* Shift window left to keep the most-recent HIST_MAX entries. */ + for (uint32 k = 0; k + 1 < INFERENCE_BRIDGE_HISTORY_MAX; k++) + qidx[k] = qidx[k + 1]; + qidx[INFERENCE_BRIDGE_HISTORY_MAX - 1] = ti; + } + } + hist_count = qcount; + for (uint32 i = 0; i < hist_count; i++) + { + /* Strings are in p_arena; they live through the synchronous bridge write. */ + hist_msgs[i].role = hist_record.turns[qidx[i]].role; + hist_msgs[i].content = hist_record.turns[qidx[i]].content; + } + } + /* --- Guest quota reservation (before persisting turn) --- */ boolean is_guest_reserved = FALSE; char quota_guest_id[37] = {0}; @@ -1797,7 +1899,15 @@ pthread_mutex_unlock(&g_pending_mutex); if (!Inference_Bridge_Start_Turn( - g_inference_bridge, request_id, conversation_id, prompt)) + g_inference_bridge, + request_id, + conversation_id, + prompt, + prompt_profile, + CONVERSATION_PROMPT_VERSION, + CONVERSATION_KNOWLEDGE_VERSION, + hist_msgs, + hist_count)) { pthread_mutex_lock(&g_pending_mutex); Pending_Turn *p_pending = Conversation_API_Find_Pending(request_id);