comparison 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
comparison
equal deleted inserted replaced
264:04fee26ecce0 265:056790c4fb0d
20 #define CONVERSATION_HISTORY_MAX (512 * 1024) 20 #define CONVERSATION_HISTORY_MAX (512 * 1024)
21 #define CONVERSATION_CUSTOM_EVENT_MAX (3 * 1024) 21 #define CONVERSATION_CUSTOM_EVENT_MAX (3 * 1024)
22 #define CONVERSATION_EVENT_NAME_MAX 64 22 #define CONVERSATION_EVENT_NAME_MAX 64
23 #define CONVERSATION_ACTIVE_MAX 4 23 #define CONVERSATION_ACTIVE_MAX 4
24 #define CONVERSATION_TURNS_PER_MINUTE 60 24 #define CONVERSATION_TURNS_PER_MINUTE 60
25 #define CONVERSATION_PROMPT_VERSION 1
26 #define CONVERSATION_KNOWLEDGE_VERSION 1
25 /* Buffer large enough for a guest Set-Cookie directive */ 27 /* Buffer large enough for a guest Set-Cookie directive */
26 #define CONV_GUEST_COOKIE_CAPACITY (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256) 28 #define CONV_GUEST_COOKIE_CAPACITY (AUTH_CRYPTO_GUEST_COOKIE_SIZE + 256)
27 29
28 /* Quota policy — read from env at init, validated at startup. */ 30 /* Quota policy — read from env at init, validated at startup. */
29 #define CONV_GUEST_DAILY_TURNS_DEFAULT 10 31 #define CONV_GUEST_DAILY_TURNS_DEFAULT 10
378 { 380 {
379 p_owner->kind = CONVERSATION_OWNER_KIND_GUEST; 381 p_owner->kind = CONVERSATION_OWNER_KIND_GUEST;
380 strncpy(p_owner->id, p_principal->guest_id, sizeof(p_owner->id) - 1); 382 strncpy(p_owner->id, p_principal->guest_id, sizeof(p_owner->id) - 1);
381 p_owner->id[sizeof(p_owner->id) - 1] = '\0'; 383 p_owner->id[sizeof(p_owner->id) - 1] = '\0';
382 } 384 }
385 }
386
387 static boolean Conversation_API_Prompt_Profile_From_Principal(
388 const Auth_Principal *p_principal,
389 Inference_Prompt_Profile *p_profile)
390 {
391 if (!p_principal || !p_profile)
392 return FALSE;
393 if (p_principal->kind == AUTH_PRINCIPAL_GUEST)
394 {
395 *p_profile = INFERENCE_PROMPT_PROFILE_PUBLIC_VISITOR;
396 return TRUE;
397 }
398 if (p_principal->kind != AUTH_PRINCIPAL_USER)
399 return FALSE;
400 if (strcmp(p_principal->role, "member") == 0)
401 {
402 *p_profile = INFERENCE_PROMPT_PROFILE_INVITED_FRIEND;
403 return TRUE;
404 }
405 if (strcmp(p_principal->role, "admin") == 0)
406 {
407 *p_profile = INFERENCE_PROMPT_PROFILE_JUNE_ADMIN;
408 return TRUE;
409 }
410 return FALSE;
383 } 411 }
384 412
385 static void Conversation_API_Send_Stream_Error( 413 static void Conversation_API_Send_Stream_Error(
386 Seobeo_Handle *p_handle, 414 Seobeo_Handle *p_handle,
387 int status, 415 int status,
1513 { 1541 {
1514 Conversation_API_Send_Stream_Error( 1542 Conversation_API_Send_Stream_Error(
1515 p_handle, 500, "internal_error", "Session error"); 1543 p_handle, 500, "internal_error", "Session error");
1516 return; 1544 return;
1517 } 1545 }
1546 Inference_Prompt_Profile prompt_profile;
1547 if (!Conversation_API_Prompt_Profile_From_Principal(
1548 &principal, &prompt_profile))
1549 {
1550 Conversation_API_Send_Stream_Error(
1551 p_handle, 403, "invalid_role", "Conversation role is not supported");
1552 return;
1553 }
1518 if (!found) 1554 if (!found)
1519 { 1555 {
1520 Conversation_API_Send_Stream_Error( 1556 Conversation_API_Send_Stream_Error(
1521 p_handle, 401, "auth_required", "Bootstrap session first"); 1557 p_handle, 401, "auth_required", "Bootstrap session first");
1522 return; 1558 return;
1601 { 1637 {
1602 Conversation_API_Release_Turn_Slot(); 1638 Conversation_API_Release_Turn_Slot();
1603 Conversation_API_Send_Stream_Error( 1639 Conversation_API_Send_Stream_Error(
1604 p_handle, 500, "id_failed", "Unable to create request ID"); 1640 p_handle, 500, "id_failed", "Unable to create request ID");
1605 return; 1641 return;
1642 }
1643
1644 /* --- Load owner-verified conversation record for transcript history --- */
1645 /* Done before quota reservation: if the conversation is missing or
1646 * not owned by this principal we fail fast with no quota side-effects. */
1647 Conversation_Record hist_record;
1648 memset(&hist_record, 0, sizeof(hist_record));
1649 {
1650 Conversation_Store_Result hist_result = Conversation_Store_Get_Owned(
1651 g_conversation_store, conversation_id, &owner, &hist_record, p_arena);
1652 if (hist_result == CONVERSATION_STORE_NOT_FOUND)
1653 {
1654 Conversation_API_Release_Turn_Slot();
1655 Conversation_API_Send_Stream_Error(
1656 p_handle, 404, "not_found", "Conversation not found");
1657 return;
1658 }
1659 if (hist_result != CONVERSATION_STORE_OK)
1660 {
1661 Conversation_API_Release_Turn_Slot();
1662 Conversation_API_Send_Stream_Error(
1663 p_handle, 500, "history_load_failed", "Unable to load conversation");
1664 return;
1665 }
1666 }
1667
1668 /* Build bounded history: last ≤20 qualifying turns from the persisted
1669 * record. Include non-empty user turns and completed, non-empty assistant
1670 * turns only. The new prompt is not yet part of history. */
1671 Inference_Bridge_History_Message hist_msgs[INFERENCE_BRIDGE_HISTORY_MAX];
1672 uint32 hist_count = 0;
1673 {
1674 /* Collect qualifying turn indices in a sliding window of HIST_MAX. */
1675 size_t qidx[INFERENCE_BRIDGE_HISTORY_MAX];
1676 uint32 qcount = 0;
1677 size_t num_turns = Dowa_Array_Length(hist_record.turns);
1678 for (size_t ti = 0; ti < num_turns; ti++)
1679 {
1680 Conversation_Turn *t = &hist_record.turns[ti];
1681 if (!t->role || !t->content || !t->status)
1682 continue;
1683 boolean is_user = strcmp(t->role, "user") == 0;
1684 boolean is_asst = strcmp(t->role, "assistant") == 0;
1685 if (!is_user && !is_asst)
1686 continue;
1687 if (t->content[0] == '\0')
1688 continue;
1689 if (is_asst && strcmp(t->status, "complete") != 0)
1690 continue;
1691 if (qcount < INFERENCE_BRIDGE_HISTORY_MAX)
1692 qidx[qcount++] = ti;
1693 else
1694 {
1695 /* Shift window left to keep the most-recent HIST_MAX entries. */
1696 for (uint32 k = 0; k + 1 < INFERENCE_BRIDGE_HISTORY_MAX; k++)
1697 qidx[k] = qidx[k + 1];
1698 qidx[INFERENCE_BRIDGE_HISTORY_MAX - 1] = ti;
1699 }
1700 }
1701 hist_count = qcount;
1702 for (uint32 i = 0; i < hist_count; i++)
1703 {
1704 /* Strings are in p_arena; they live through the synchronous bridge write. */
1705 hist_msgs[i].role = hist_record.turns[qidx[i]].role;
1706 hist_msgs[i].content = hist_record.turns[qidx[i]].content;
1707 }
1606 } 1708 }
1607 1709
1608 /* --- Guest quota reservation (before persisting turn) --- */ 1710 /* --- Guest quota reservation (before persisting turn) --- */
1609 boolean is_guest_reserved = FALSE; 1711 boolean is_guest_reserved = FALSE;
1610 char quota_guest_id[37] = {0}; 1712 char quota_guest_id[37] = {0};
1795 p_turn->p_next = g_pending_turns; 1897 p_turn->p_next = g_pending_turns;
1796 g_pending_turns = p_turn; 1898 g_pending_turns = p_turn;
1797 pthread_mutex_unlock(&g_pending_mutex); 1899 pthread_mutex_unlock(&g_pending_mutex);
1798 1900
1799 if (!Inference_Bridge_Start_Turn( 1901 if (!Inference_Bridge_Start_Turn(
1800 g_inference_bridge, request_id, conversation_id, prompt)) 1902 g_inference_bridge,
1903 request_id,
1904 conversation_id,
1905 prompt,
1906 prompt_profile,
1907 CONVERSATION_PROMPT_VERSION,
1908 CONVERSATION_KNOWLEDGE_VERSION,
1909 hist_msgs,
1910 hist_count))
1801 { 1911 {
1802 pthread_mutex_lock(&g_pending_mutex); 1912 pthread_mutex_lock(&g_pending_mutex);
1803 Pending_Turn *p_pending = Conversation_API_Find_Pending(request_id); 1913 Pending_Turn *p_pending = Conversation_API_Find_Pending(request_id);
1804 if (p_pending) 1914 if (p_pending)
1805 { 1915 {