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);