All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 29s
CI Code / Code Coverage (push) Successful in 2m44s
CI Code / Linux (debian) (push) Successful in 4m46s
CI Code / Linux (ubuntu) (push) Successful in 4m59s
CI Code / Linux (arch) (push) Successful in 5m56s
Add an AI client module that integrates with OpenAI-compatible API providers (OpenAI, Perplexity, and custom endpoints) to provide AI-assisted chat within CProof. Users can start sessions with /ai start, send prompts, receive responses in a dedicated AI window, switch between providers and models, and manage API keys — all with tab-completion. Providers are configured via /ai set commands with per-provider API keys, endpoints, default models, and custom settings. Two default providers (openai, perplexity) are seeded on first use. Provider state persists in [ai/<name>] sections of the preferences keyfile with automatic migration from the previous flat-key format. The /ai command integrates into the existing command system with 8 subcommands covering provider management, session lifecycle, model fetching, and conversation clearing. Autocomplete uses the standard flat prefix-matching chain for reliable tab-completion at every nesting level. A new ProfAiWin window type is added to the window system. Architecture: Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s) Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM JSON parsing uses unified helpers for both chat responses and error envelopes with consistent escape decoding. The response parser tries Perplexity /v1/responses "text" field first, then falls back to OpenAI "content". Error parsing extracts provider error.message from the standard envelope format. Model parsing handles multiple API response formats (OpenAI list, Perplexity, array) including edge cases. Tests include 470+ lines of unit tests covering provider management, session lifecycle, JSON parsing (multiple formats), autocomplete cycling, and error handling, plus functional tests for /ai command dispatch. A stub_ai.c module isolates unit tests from UI dependencies.
292 lines
9.3 KiB
C
292 lines
9.3 KiB
C
#ifndef AI_CLIENT_H
|
|
#define AI_CLIENT_H
|
|
|
|
#include <glib.h>
|
|
|
|
/**
|
|
* @brief AI message structure for conversation history.
|
|
*/
|
|
typedef struct ai_message_t
|
|
{
|
|
gchar* role; /* "user" or "assistant" */
|
|
gchar* content; /* Message content */
|
|
} AIMessage;
|
|
|
|
/**
|
|
* @brief AI provider configuration.
|
|
*/
|
|
typedef struct ai_provider_t
|
|
{
|
|
gchar* name; /* Provider name (e.g., "openai", "perplexity") */
|
|
gchar* api_url; /* API endpoint URL */
|
|
gchar* project_id; /* Optional project ID (for some providers) */
|
|
gchar* default_model; /* Default model for this provider */
|
|
GHashTable* settings; /* Extensible per-provider settings (e.g., tools=enabled, search=disabled) */
|
|
GList* models; /* Cached models (gchar*) */
|
|
gboolean models_fresh; /* Whether models cache is current */
|
|
gint32 ref_count; /* Reference count (atomic) */
|
|
} AIProvider;
|
|
|
|
/**
|
|
* @brief AI chat session structure.
|
|
*/
|
|
typedef struct ai_session_t
|
|
{
|
|
pthread_mutex_t lock; /* Protects all session fields below */
|
|
gchar* provider_name; /* Provider name */
|
|
AIProvider* provider; /* Provider configuration */
|
|
gchar* model; /* Model identifier (e.g., "gpt-4", "sonar") */
|
|
gchar* api_key; /* API key for this session */
|
|
GList* history; /* Conversation history (GList of AIMessage*) */
|
|
gint32 ref_count; /* Reference count (atomic) */
|
|
} AISession;
|
|
|
|
/* ========================================================================
|
|
* Provider Management
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Initialize the AI client and load default providers.
|
|
*/
|
|
void ai_client_init(void);
|
|
|
|
/**
|
|
* Shutdown the AI client and free resources.
|
|
*/
|
|
void ai_client_shutdown(void);
|
|
|
|
/**
|
|
* Get a provider by name.
|
|
* @param name The provider name (e.g., "openai", "perplexity")
|
|
* @return AIProvider*, or NULL if not found
|
|
*/
|
|
AIProvider* ai_get_provider(const gchar* name);
|
|
|
|
/**
|
|
* Add or update a provider configuration.
|
|
* @param name The provider name
|
|
* @param api_url The API endpoint URL
|
|
* @return New AIProvider* (caller must unref when done)
|
|
*/
|
|
AIProvider* ai_add_provider(const gchar* name, const gchar* api_url);
|
|
|
|
/**
|
|
* Remove a provider by name.
|
|
* @param name The provider name
|
|
* @return TRUE if provider was removed, FALSE if not found
|
|
*/
|
|
gboolean ai_remove_provider(const gchar* name);
|
|
|
|
/**
|
|
* Increment the reference count of a provider.
|
|
* @param provider The provider to reference
|
|
* @return The same provider pointer
|
|
*/
|
|
AIProvider* ai_provider_ref(AIProvider* provider);
|
|
|
|
/**
|
|
* Decrement the reference count of a provider.
|
|
* @param provider The provider to unreference
|
|
*/
|
|
void ai_provider_unref(AIProvider* provider);
|
|
|
|
/**
|
|
* List all configured providers.
|
|
* @return GList of AIProvider* (caller must not free the list or providers,
|
|
* and must not unref the returned providers)
|
|
*/
|
|
GList* ai_list_providers(void);
|
|
|
|
/**
|
|
* Find a provider name for autocomplete.
|
|
* @param search_str The search string
|
|
* @param previous Whether to go to previous match
|
|
* @param context Unused
|
|
* @return Provider name, or NULL if not found
|
|
*/
|
|
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
|
|
|
/**
|
|
* Reset the provider autocomplete state.
|
|
* Called from cmd_ac_reset() to clear autocomplete state.
|
|
*/
|
|
void ai_providers_reset_ac(void);
|
|
|
|
/**
|
|
* Get the API key for a provider.
|
|
* @param provider_name The provider name
|
|
* @return The API key, or NULL if not set (caller must free)
|
|
*/
|
|
gchar* ai_get_provider_key(const gchar* provider_name);
|
|
|
|
/**
|
|
* Set the API key for a provider.
|
|
* @param provider_name The provider name
|
|
* @param api_key The API key to set
|
|
*/
|
|
void ai_set_provider_key(const gchar* provider_name, const gchar* api_key);
|
|
|
|
/**
|
|
* Set the default model for a provider.
|
|
* @param provider_name The provider name
|
|
* @param model The default model name
|
|
*/
|
|
void ai_set_provider_default_model(const gchar* provider_name, const gchar* model);
|
|
|
|
/**
|
|
* Get the default model for a provider.
|
|
* @param provider_name The provider name
|
|
* @return The default model name (caller must not free), or NULL if not set
|
|
*/
|
|
const gchar* ai_get_provider_default_model(const gchar* provider_name);
|
|
|
|
/**
|
|
* Set a custom setting for a provider.
|
|
* @param provider_name The provider name
|
|
* @param setting The setting key (e.g., "tools", "search")
|
|
* @param value The setting value
|
|
*/
|
|
void ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const gchar* value);
|
|
|
|
/**
|
|
* Get a custom setting for a provider.
|
|
* @param provider_name The provider name
|
|
* @param setting The setting key
|
|
* @return The setting value (caller must free), or NULL if not set
|
|
*/
|
|
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
|
|
|
|
/**
|
|
* Fetch available models from a provider's API.
|
|
* @param provider_name The provider name
|
|
* @param user_data User data (ProfAiWin* for UI display)
|
|
* @return TRUE if the request was successfully queued, FALSE otherwise
|
|
*/
|
|
gboolean ai_fetch_models(const gchar* provider_name, gpointer user_data);
|
|
|
|
/**
|
|
* Check if models cache is fresh for a provider.
|
|
* @param provider_name The provider name
|
|
* @return TRUE if models are fresh, FALSE otherwise
|
|
*/
|
|
gboolean ai_models_are_fresh(const gchar* provider_name);
|
|
|
|
/* ========================================================================
|
|
* Parsing helpers (exposed for testing)
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Parse model IDs from an OpenAI-compatible API response.
|
|
* Expected format: {"object":"list","data":[{"id":"model1",...},...]}
|
|
* @param provider The provider to add models to
|
|
* @param json The JSON response from the API
|
|
*/
|
|
void ai_parse_models_from_json(AIProvider* provider, const gchar* json);
|
|
|
|
/**
|
|
* Extract assistant content from an LLM response body. Tries Perplexity
|
|
* /v1/responses "text" first, falls back to OpenAI "content". Decodes
|
|
* the \" and \n escape sequences only.
|
|
* @param response_json The JSON body from the provider
|
|
* @return Newly allocated content string, or NULL if not found (caller frees)
|
|
*/
|
|
gchar* ai_parse_response(const gchar* response_json);
|
|
|
|
/**
|
|
* Extract human-readable error message from an API error envelope.
|
|
* Expected format: {"error":{"message":"...","type":"...","code":...}}
|
|
* Decodes \" \n \\ \t escapes.
|
|
* @param error_json The JSON body of the error response
|
|
* @return Newly allocated error message, or NULL if not parseable (caller frees)
|
|
*/
|
|
gchar* ai_parse_error_message(const gchar* error_json);
|
|
|
|
/* ========================================================================
|
|
* Session Management
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Create a new AI session with the specified provider and model.
|
|
* @param provider_name The provider name (e.g., "openai")
|
|
* @param model The model identifier (e.g., "gpt-4")
|
|
* @return New AISession*, or NULL on failure
|
|
*/
|
|
AISession* ai_session_create(const gchar* provider_name, const gchar* model);
|
|
|
|
/**
|
|
* Increment the reference count of an AI session.
|
|
* @param session The session to reference
|
|
* @return The same session pointer
|
|
*/
|
|
AISession* ai_session_ref(AISession* session);
|
|
|
|
/**
|
|
* Decrement the reference count and free the session when it reaches zero.
|
|
* @param session The session to unreference
|
|
*/
|
|
void ai_session_unref(AISession* session);
|
|
|
|
/**
|
|
* Add a message to the session history.
|
|
* @param session The session
|
|
* @param role The message role ("user" or "assistant")
|
|
* @param content The message content
|
|
*/
|
|
void ai_session_add_message(AISession* session, const gchar* role, const gchar* content);
|
|
|
|
/**
|
|
* Clear the conversation history.
|
|
* @param session The session
|
|
*/
|
|
void ai_session_clear_history(AISession* session);
|
|
|
|
/**
|
|
* Get the current model for a session.
|
|
* @param session The session
|
|
* @return The model name (caller must not free)
|
|
*/
|
|
const gchar* ai_session_get_model(AISession* session);
|
|
|
|
/**
|
|
* Set the model for a session.
|
|
* @param session The session
|
|
* @param model The model name
|
|
*/
|
|
void ai_session_set_model(AISession* session, const gchar* model);
|
|
|
|
/**
|
|
* Atomically switch session provider, model, and API key.
|
|
* All mutations happen under the session lock to prevent races with
|
|
* _ai_request_thread() which snapshots session state before making requests.
|
|
*
|
|
* @param session The session
|
|
* @param provider_name New provider name
|
|
* @param model New model identifier
|
|
* @param api_key New API key (caller must free after calling this)
|
|
*/
|
|
void ai_session_switch(AISession* session, const gchar* provider_name,
|
|
const gchar* model, gchar* api_key);
|
|
|
|
/* ========================================================================
|
|
* Request Handling
|
|
* ======================================================================== */
|
|
|
|
/**
|
|
* Send a prompt to the AI provider asynchronously.
|
|
* @param session The AI session containing provider and model
|
|
* @param prompt The prompt to send
|
|
* @param user_data User data (ProfAiWin* for UI display)
|
|
* @return TRUE if the request was successfully queued, FALSE otherwise
|
|
*/
|
|
gboolean ai_send_prompt(AISession* session, const gchar* prompt,
|
|
gpointer user_data);
|
|
|
|
/**
|
|
* Escape a string for JSON embedding.
|
|
* @param str The string to escape
|
|
* @return Newly allocated escaped string (caller must free)
|
|
*/
|
|
gchar* ai_json_escape(const gchar* str);
|
|
|
|
#endif /* AI_CLIENT_H */
|