Mercurial
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 { |