mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-21 13:46:21 +00:00
feat(ai): add AI client with multi-provider support and UI
Add an AI client module that integrates with OpenAI-compatible API providers (OpenAI, Perplexity, and custom providers) to provide AI-assisted responses within the profanity client. The implementation includes: - src/ai/ai_client.c/h: Core AI client with provider management, session handling, and async HTTP request handling via libcurl. Supports per-provider API keys stored in preferences, reference- counted sessions, and conversation history tracking. - src/ui/window.c/window_list.c: New AI window type (ProfAiWin) for displaying AI conversations, with response streaming and error display capabilities. - Command integration: New `/ai` command (cmd_defs.c, cmd_funcs.c) for creating sessions, sending prompts, and managing providers. Provider autocomplete support in cmd_ac.c. - Preferences integration: API keys for providers are persisted in the preferences system (config/preferences.c). - Unit tests: 472 lines of comprehensive tests covering provider management, session lifecycle, JSON escaping, and autocomplete (tests/unittests/test_ai_client.c). Architecture decisions: - Asynchronous design: HTTP requests run on a separate thread to avoid blocking the main UI loop. Callbacks are invoked on the main thread via direct function call (profanity uses ncurses, not GLib main loop). - Reference counting: Both AIProvider and AISession use ref counting for safe shared ownership. - Response size limit: 10MB cap on HTTP responses to prevent OOM.
This commit is contained in:
@@ -66,6 +66,8 @@
|
||||
#include "omemo/omemo.h"
|
||||
#endif
|
||||
|
||||
#include "ai/ai_client.h"
|
||||
|
||||
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -137,6 +139,7 @@ static char* _strophe_autocomplete(ProfWin* window, const char* const input, gbo
|
||||
static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
|
||||
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
|
||||
|
||||
@@ -278,6 +281,9 @@ static Autocomplete privacy_log_ac;
|
||||
static Autocomplete color_ac;
|
||||
static Autocomplete correction_ac;
|
||||
static Autocomplete avatar_ac;
|
||||
static Autocomplete ai_subcommands_ac;
|
||||
static Autocomplete ai_set_subcommands_ac;
|
||||
static Autocomplete ai_remove_subcommands_ac;
|
||||
static Autocomplete url_ac;
|
||||
static Autocomplete executable_ac;
|
||||
static Autocomplete executable_param_ac;
|
||||
@@ -452,7 +458,10 @@ static Autocomplete* all_acs[] = {
|
||||
&vcard_togglable_param_ac,
|
||||
&vcard_address_type_ac,
|
||||
&force_encryption_ac,
|
||||
&force_encryption_policy_ac
|
||||
&force_encryption_policy_ac,
|
||||
&ai_subcommands_ac,
|
||||
&ai_set_subcommands_ac,
|
||||
&ai_remove_subcommands_ac
|
||||
};
|
||||
|
||||
static GHashTable* ac_funcs = NULL;
|
||||
@@ -1153,6 +1162,19 @@ cmd_ac_init(void)
|
||||
autocomplete_add(correction_ac, "off");
|
||||
autocomplete_add(correction_ac, "char");
|
||||
|
||||
autocomplete_add(ai_subcommands_ac, "set");
|
||||
autocomplete_add(ai_subcommands_ac, "remove");
|
||||
autocomplete_add(ai_subcommands_ac, "start");
|
||||
autocomplete_add(ai_subcommands_ac, "clear");
|
||||
autocomplete_add(ai_subcommands_ac, "correct");
|
||||
autocomplete_add(ai_subcommands_ac, "providers");
|
||||
|
||||
autocomplete_add(ai_set_subcommands_ac, "provider");
|
||||
autocomplete_add(ai_set_subcommands_ac, "token");
|
||||
autocomplete_add(ai_set_subcommands_ac, "org");
|
||||
|
||||
autocomplete_add(ai_remove_subcommands_ac, "provider");
|
||||
|
||||
autocomplete_add(avatar_ac, "set");
|
||||
autocomplete_add(avatar_ac, "disable");
|
||||
autocomplete_add(avatar_ac, "get");
|
||||
@@ -1428,6 +1450,7 @@ cmd_ac_init(void)
|
||||
g_hash_table_insert(ac_funcs, "/wins", _wins_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/wintitle", _wintitle_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/force-encryption", _force_encryption_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4415,5 +4438,93 @@ _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolea
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/force-encryption", force_encryption_ac, TRUE, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
gboolean parsed = FALSE;
|
||||
|
||||
auto_gcharv gchar** args = parse_args(input, 0, 3, &parsed);
|
||||
|
||||
if (parsed) {
|
||||
int num_args = g_strv_length(args);
|
||||
|
||||
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
|
||||
if (num_args >= 1) {
|
||||
/* Try to match subcommand prefix first */
|
||||
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (g_strcmp0(args[0], "set") == 0) {
|
||||
if (num_args >= 2) {
|
||||
if (g_strcmp0(args[1], "provider") == 0) {
|
||||
// /ai set provider <name> <url> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else if (g_strcmp0(args[1], "token") == 0) {
|
||||
// /ai set token <provider> <token> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set token", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else if (g_strcmp0(args[1], "org") == 0) {
|
||||
// /ai set org <provider> <org_id> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai set org", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// /ai set <subcommand> - autocomplete subcommands
|
||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (g_strcmp0(args[0], "remove") == 0) {
|
||||
if (num_args >= 2 && g_strcmp0(args[1], "provider") == 0) {
|
||||
// /ai remove provider <name> - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai remove provider", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
result = autocomplete_param_with_ac(input, "/ai remove", ai_remove_subcommands_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (g_strcmp0(args[0], "start") == 0) {
|
||||
// /ai start [<provider>/<model>] - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai start", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else if (g_strcmp0(args[0], "clear") == 0) {
|
||||
// /ai clear [<provider>] - autocomplete provider names
|
||||
result = autocomplete_param_with_func(input, "/ai clear", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else if (g_strcmp0(args[0], "correct") == 0) {
|
||||
// /ai correct <provider> <text>
|
||||
result = autocomplete_param_with_func(input, "/ai correct", ai_providers_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} else if (g_strcmp0(args[0], "providers") == 0) {
|
||||
// /ai providers - no subcommands
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user