From 063b2ccdc83ebebe2c510a78abdee9d6ebb9f342 Mon Sep 17 00:00:00 2001 From: Jabber Developer Date: Fri, 1 May 2026 19:01:31 +0000 Subject: [PATCH] (broken) add json parsing for ai --- Dockerfile.arch | 1 + Dockerfile.debian | 1 + Dockerfile.fedora | 1 + Dockerfile.tumbleweed | 1 + Dockerfile.ubuntu | 1 + Makefile.am | 2 +- configure.ac | 8 +- docs/json-glib-integration-plan.md | 314 +++++++++++++++++++++++++++++ src/ai/ai_client.c | 157 +++++++++------ 9 files changed, 416 insertions(+), 70 deletions(-) create mode 100644 docs/json-glib-integration-plan.md diff --git a/Dockerfile.arch b/Dockerfile.arch index 7f1088b1..72c77941 100644 --- a/Dockerfile.arch +++ b/Dockerfile.arch @@ -41,6 +41,7 @@ RUN pacman -S --needed --noconfirm \ python \ wget \ sqlite \ + json-glib \ valgrind \ gdk-pixbuf2 \ qrencode diff --git a/Dockerfile.debian b/Dockerfile.debian index 93c14407..bc7a62e1 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ python-dev-is-python3 \ valgrind \ libsqlite3-dev \ + libjson-glib-dev \ libgdk-pixbuf-2.0-dev \ libqrencode-dev diff --git a/Dockerfile.fedora b/Dockerfile.fedora index 4bfadc4a..12a57a5a 100644 --- a/Dockerfile.fedora +++ b/Dockerfile.fedora @@ -38,6 +38,7 @@ RUN dnf install -y \ readline-devel \ openssl-devel \ sqlite-devel \ + json-glib-devel \ valgrind \ gdk-pixbuf2-devel \ qrencode-devel diff --git a/Dockerfile.tumbleweed b/Dockerfile.tumbleweed index 732e9e26..360679c9 100644 --- a/Dockerfile.tumbleweed +++ b/Dockerfile.tumbleweed @@ -37,6 +37,7 @@ RUN zypper --non-interactive in --no-recommends \ python310-devel \ readline-devel \ sqlite3-devel \ + json-glib-devel \ valgrind \ gdk-pixbuf-devel \ qrencode-devel diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index c4ff309e..deab2008 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -34,6 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ python-dev-is-python3 \ valgrind \ libsqlite3-dev \ + libjson-glib-dev \ libgdk-pixbuf-2.0-dev \ libqrencode-dev diff --git a/Makefile.am b/Makefile.am index 644a3320..613d8af0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -296,7 +296,7 @@ TESTS = tests/unittests/unittests check_PROGRAMS = tests/unittests/unittests tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests tests_unittests_unittests_SOURCES = $(unittest_sources) -tests_unittests_unittests_LDADD = -lcmocka +tests_unittests_unittests_LDADD = -lcmocka $(json-glib_LIBS) # Functional tests require libstabber. # They are only built when libstabber is available. diff --git a/configure.ac b/configure.ac index 9858a117..b9198215 100644 --- a/configure.ac +++ b/configure.ac @@ -97,6 +97,10 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [], PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [], [AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])]) +## Check for json-glib (JSON parsing for AI responses) +PKG_CHECK_MODULES([json-glib], [json-glib-1.0 >= 1.6.0], [], + [AC_MSG_ERROR([json-glib 1.6.0 or higher is required])]) + ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])]) AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ]) @@ -429,11 +433,11 @@ AS_IF([test "x$PACKAGE_STATUS" = xdevelopment], AS_IF([test "x$PLATFORM" = xosx], [AM_CFLAGS="$AM_CFLAGS -Qunused-arguments"]) -AM_CFLAGS="$AM_CFLAGS $PTHREAD_CFLAGS $glib_CFLAGS $gio_CFLAGS $curl_CFLAGS ${SQLITE_CFLAGS}" +AM_CFLAGS="$AM_CFLAGS $PTHREAD_CFLAGS $glib_CFLAGS $gio_CFLAGS $curl_CFLAGS ${SQLITE_CFLAGS} $json_glib_CFLAGS" AM_CFLAGS="$AM_CFLAGS $libnotify_CFLAGS ${GTK_CFLAGS} $python_CFLAGS" AM_CFLAGS="$AM_CFLAGS -DTHEMES_PATH=\"\\\"$THEMES_PATH\\\"\" -DICONS_PATH=\"\\\"$ICONS_PATH\\\"\" -DGLOBAL_PYTHON_PLUGINS_PATH=\"\\\"$GLOBAL_PYTHON_PLUGINS_PATH\\\"\" -DGLOBAL_C_PLUGINS_PATH=\"\\\"$GLOBAL_C_PLUGINS_PATH\\\"\"" -LIBS="$glib_LIBS $gio_LIBS $PTHREAD_LIBS $curl_LIBS $libnotify_LIBS $python_LIBS ${GTK_LIBS} ${SQLITE_LIBS} $LIBS" +LIBS="$glib_LIBS $gio_LIBS $PTHREAD_LIBS $curl_LIBS $libnotify_LIBS $python_LIBS ${GTK_LIBS} ${SQLITE_LIBS} $json_glib_LIBS $LIBS" AC_SUBST(AM_LDFLAGS) AC_SUBST(AM_CFLAGS) diff --git a/docs/json-glib-integration-plan.md b/docs/json-glib-integration-plan.md new file mode 100644 index 00000000..1c13da94 --- /dev/null +++ b/docs/json-glib-integration-plan.md @@ -0,0 +1,314 @@ +# JSON-GLib Integration Plan for Issue #15 + +## Overview + +This document outlines the complete plan for integrating json-glib into the CProof (profanity) build system and codebase. The integration replaces the `strstr`-based JSON parsing in [`_parse_ai_response()`](src/ai/ai_client.c:587) with proper json-glib parsing, and adds comprehensive unit tests. + +--- + +## Current State Analysis + +### What Has Already Been Done + +| Component | Status | Details | +|-----------|--------|---------| +| `configure.ac` | DONE | `PKG_CHECK_MODULES([json-glib], [json-glib-1.0 >= 1.6.0])` added at line 101-102 | +| `src/ai/ai_client.c` | DONE | `#include ` added at line 24; [`_parse_ai_response()`](src/ai/ai_client.c:587) rewritten using json-glib | +| `Dockerfile.ubuntu` | DONE | `libjson-glib-dev` added at line 37 | +| `Dockerfile.debian` | DONE | `libjson-glib-dev` added at line 38 | + +### What Is Missing (Root Causes of `bootstrap.sh` Failure) + +1. **`Makefile.am` missing json-glib flags:** + - `AM_CFLAGS` at line 272 does not include `$(json-glib_CFLAGS)` + - `tests_unittests_unittests_LDADD` at line 299 does not include `$(json-glib_LIBS)` + - The main `profanity` binary target also needs `$(json-glib_LIBS)` in its link flags + +2. **No unit tests for `_parse_ai_response()`:** + - [`test_ai_client.c`](tests/unittests/test_ai_client.c) has 464 lines of tests but none for the JSON parsing function + - The function is `static`, so it cannot be tested directly; a test harness or refactoring is needed + +3. **`bootstrap.sh` failure:** + - [`bootstrap.sh`](bootstrap.sh) is a simple script: `mkdir -p m4 && autoreconf -i "$@"` + - The failure is caused by `autoreconf` failing due to missing json-glib m4 macros in the `m4/` directory, or the `configure.ac` referencing `PKG_CHECK_MODULES` without the proper `ax_pthread.m4` or json-glib pkg-config files being available in the build environment + +--- + +## Detailed Plan + +### 1. Build System Changes + +#### 1.1 `configure.ac` — No Changes Needed + +The json-glib check is already present: + +```autoconf +PKG_CHECK_MODULES([json-glib], [json-glib-1.0 >= 1.6.0], [], + [AC_MSG_ERROR([json-glib 1.6.0 or higher is required])]) +``` + +This generates the variables `json-glib_CFLAGS` and `json-glib_LIBS` for use in `Makefile.am`. + +#### 1.2 `Makefile.am` — Changes Required + +**File:** [`Makefile.am`](Makefile.am) + +**Change 1: Add json-glib_CFLAGS to AM_CFLAGS (line 272)** + +```makefile +# Current (line 272): +AM_CFLAGS = @AM_CFLAGS@ -I$(srcdir)/src + +# Change to: +AM_CFLAGS = @AM_CFLAGS@ $(json-glib_CFLAGS) -I$(srcdir)/src +``` + +**Change 2: Add json-glib_LIBS to profanity linking (line 275)** + +The `profanity` binary target needs json-glib_LIBS. Since `profanity_SOURCES` is defined at line 275 and no explicit `profanity_LDADD` exists, we need to add one: + +```makefile +# Add after line 275: +profanity_LDADD = $(json-glib_LIBS) $(LIBS) +``` + +**Change 3: Add json-glib_LIBS to test linking (line 299)** + +```makefile +# Current (line 299): +tests_unittests_unittests_LDADD = -lcmocka + +# Change to: +tests_unittests_unittests_LDADD = -lcmocka $(json-glib_LIBS) +``` + +#### 1.3 Why `bootstrap.sh` Fails + +The [`bootstrap.sh`](bootstrap.sh) script runs `autoreconf -i`, which regenerates the `configure` script from `configure.ac`. The failure occurs because: + +1. **pkg-config dependency:** `PKG_CHECK_MODULES` requires `pkg-config` to be available at configure-time (not build-time). The `autoreconf` step itself should succeed, but the resulting `configure` script will fail if `pkg-config --modversion json-glib-1.0 >= 1.6.0` cannot find the json-glib installation. + +2. **Likely root cause:** The build environment (Docker container or host) does not have `json-glib` development files installed, or `pkg-config` cannot locate the `json-glib-1.0.pc` file. + +**Fix:** Ensure `libjson-glib-dev` (Debian/Ubuntu) or `json-glib` (Arch/Fedora) is installed in the build environment before running `bootstrap.sh`. + +--- + +### 2. Source Code Changes + +#### 2.1 `src/ai/ai_client.c` — No Changes Needed + +The [`_parse_ai_response()`](src/ai/ai_client.c:587) function has already been rewritten using json-glib: + +```c +static gchar* +_parse_ai_response(const gchar* response_json) +{ + // ... json-glib parsing with three fallback strategies: + // Try 1: Perplexity /v1/agent format + // Try 2: OpenAI /v1/chat/completions format + // Try 3: Fallback "text" field +} +``` + +#### 2.2 `src/ai/ai_client.h` — No Changes Needed + +The header file already includes `` and defines all necessary types. No json-glib-specific types are exposed (json-glib is an implementation detail). + +--- + +### 3. Test Changes + +#### 3.1 `tests/unittests/test_ai_client.c` — New Tests Required + +The current test file has 464 lines covering provider management, session handling, JSON escaping, and autocomplete. **No tests exist for `_parse_ai_response()`** because it is `static`. + +**Option A: Refactor `_parse_ai_response` to be testable (Recommended)** + +Make the function non-static and add a test wrapper: + +```c +// In ai_client.h, add: +gchar* ai_parse_ai_response(const gchar* response_json); + +// In ai_client.c, change: +// static gchar* _parse_ai_response(...) → gchar* ai_parse_ai_response(...) +``` + +**Option B: Use a test harness with `extern` declaration** + +Add to test file: +```c +extern gchar* _parse_ai_response(const gchar* response_json); +``` + +**Recommended tests to add:** + +| Test Name | Description | +|-----------|-------------| +| `test_ai_parse_response_null` | NULL input returns NULL | +| `test_ai_parse_response_empty` | Empty string returns NULL | +| `test_ai_parse_response_invalid_json` | Malformed JSON returns NULL | +| `test_ai_parse_response_openai_format` | Valid OpenAI `/v1/chat/completions` response | +| `test_ai_parse_response_openai_empty_choices` | Response with empty choices array | +| `test_ai_parse_response_perplexity_format` | Valid Perplexity `/v1/agent` response | +| `test_ai_parse_response_perplexity_empty_output` | Response with empty output array | +| `test_ai_parse_response_text_fallback` | Response with only "text" field | +| `test_ai_parse_response_unicode_content` | Content with unicode characters | +| `test_ai_parse_response_multiline_content` | Content with newlines and tabs | +| `test_ai_parse_response_html_content` | Content with HTML tags | +| `test_ai_parse_response_error_format` | Response with "error" field (not "choices") | + +#### 3.2 `tests/unittests/test_ai_client.h` — Update Required + +Add declarations for new test functions: + +```c +/* JSON parsing tests */ +void test_ai_parse_response_null(void** state); +void test_ai_parse_response_empty(void** state); +void test_ai_parse_response_invalid_json(void** state); +void test_ai_parse_response_openai_format(void** state); +void test_ai_parse_response_openai_empty_choices(void** state); +void test_ai_parse_response_perplexity_format(void** state); +void test_ai_parse_response_perplexity_empty_output(void** state); +void test_ai_parse_response_text_fallback(void** state); +void test_ai_parse_response_unicode_content(void** state); +void test_ai_parse_response_multiline_content(void** state); +void test_ai_parse_response_html_content(void** state); +void test_ai_parse_response_error_format(void** state); +``` + +#### 3.3 `tests/unittests/unittests.c` — Update Required + +Add new test entries after the existing AI client tests (around line 696): + +```c +// After line 696 (test_ai_json_escape_backslash_quote): +cmocka_unit_test(test_ai_parse_response_null), +cmocka_unit_test(test_ai_parse_response_empty), +cmocka_unit_test(test_ai_parse_response_invalid_json), +cmocka_unit_test(test_ai_parse_response_openai_format), +cmocka_unit_test(test_ai_parse_response_openai_empty_choices), +cmocka_unit_test(test_ai_parse_response_perplexity_format), +cmocka_unit_test(test_ai_parse_response_perplexity_empty_output), +cmocka_unit_test(test_ai_parse_response_text_fallback), +cmocka_unit_test(test_ai_parse_response_unicode_content), +cmocka_unit_test(test_ai_parse_response_multiline_content), +cmocka_unit_test(test_ai_parse_response_html_content), +cmocka_unit_test(test_ai_parse_response_error_format), +``` + +--- + +### 4. Dockerfile Changes + +#### 4.1 `Dockerfile.ubuntu` — No Changes Needed + +`libjson-glib-dev` is already present at line 37. + +#### 4.2 `Dockerfile.debian` — No Changes Needed + +`libjson-glib-dev` is already present at line 38. + +#### 4.3 `Dockerfile.fedora` — Check Required + +Verify `json-glib-devel` is installed. If not, add: + +```dockerfile +json-glib-devel \ +``` + +#### 4.4 `Dockerfile.arch` — Check Required + +Verify `json-glib` is installed. If not, add: + +```dockerfile +json-glib \ +``` + +--- + +### 5. Bootstrap.sh Fix + +#### 5.1 Current State + +[`bootstrap.sh`](bootstrap.sh): +```sh +#!/bin/sh +mkdir -p m4 +autoreconf -i "$@" +``` + +#### 5.2 Root Cause Analysis + +The script itself is correct. The failure is due to one of: + +1. **Missing `json-glib-1.0.pc`**: `pkg-config` cannot find json-glib in the build environment +2. **Missing `m4/ax_pthread.m4`**: The `configure.ac` uses `ACX_PTHREAD` which requires the `ax_pthread.m4` macro +3. **Missing `m4/` macros**: Other autoconf macros referenced in `configure.ac` are not present + +#### 5.3 Fix Steps + +1. **Ensure json-glib-dev is installed** in the build environment +2. **Verify m4 macros exist:** + ```bash + ls -la m4/ + # Should contain: ax_pthread.m4, ax_valgrind_check.m4, etc. + ``` +3. **If m4 macros are missing**, they need to be copied from the system: + ```bash + # For Debian/Ubuntu: + cp /usr/share/autoconf-archive/m4/ax_pthread.m4 m4/ + cp /usr/share/autoconf-archive/m4/ax_valgrind_check.m4 m4/ + ``` +4. **Test bootstrap.sh manually:** + ```bash + ./bootstrap.sh + ./configure + make + ``` + +--- + +## Implementation Order + +```mermaid +graph TD + A[1. Fix Makefile.am] --> B[2. Verify bootstrap.sh works] + B --> C[3. Add test functions to test_ai_client.c] + C --> D[4. Update test_ai_client.h] + D --> E[5. Register tests in unittests.c] + E --> F[6. Verify Dockerfiles have json-glib-dev] + F --> G[7. Run make check to verify all tests pass] +``` + +--- + +## Summary of Changes + +| File | Change Type | Lines Affected | +|------|-------------|----------------| +| `Makefile.am` | Modify | Line 272 (AM_CFLAGS), Line 275 (add profanity_LDADD), Line 299 (LDADD) | +| `src/ai/ai_client.c` | None | Already complete | +| `src/ai/ai_client.h` | None | Already complete | +| `tests/unittests/test_ai_client.c` | Add | ~200 lines of new test functions | +| `tests/unittests/test_ai_client.h` | Add | ~12 new declarations | +| `tests/unittests/unittests.c` | Add | ~12 new test entries | +| `Dockerfile.ubuntu` | None | Already has libjson-glib-dev | +| `Dockerfile.debian` | None | Already has libjson-glib-dev | +| `Dockerfile.fedora` | Check | Verify json-glib-devel present | +| `Dockerfile.arch` | Check | Verify json-glib present | +| `bootstrap.sh` | Diagnose | Fix is environmental, not script change | + +--- + +## Verification Checklist + +- [ ] `Makefile.am` updated with `$(json-glib_CFLAGS)` and `$(json-glib_LIBS)` +- [ ] `bootstrap.sh` runs successfully +- [ ] `./configure` completes without json-glib errors +- [ ] `make` compiles without errors +- [ ] `make check` runs all tests including new json-glib tests +- [ ] All existing tests still pass +- [ ] Docker builds succeed with json-glib integration diff --git a/src/ai/ai_client.c b/src/ai/ai_client.c index ea2f89e8..6a1280ea 100644 --- a/src/ai/ai_client.c +++ b/src/ai/ai_client.c @@ -21,6 +21,7 @@ #include #include +#include #include /* Default providers */ @@ -582,87 +583,109 @@ _build_json_payload(AISession* session, const gchar* prompt) return json_payload; } -static gchar* -_parse_ai_response(const gchar* response_json) +/** @brief Parse AI response JSON and extract content string. + * + * Tries multiple JSON paths to handle different provider response formats: + * 1. Perplexity /v1/agent: {"output":[{"content":[{"text":"..."}]}]} + * 2. OpenAI /v1/chat/completions: {"choices":[{"message":{"content":"..."}}]} + * 3. Fallback: root-level "text" field + * 4. Fallback: root-level "content" field + * + * @param response_json The JSON response string + * @return Newly allocated content string, or NULL on failure + */ +gchar* +ai_parse_response(const gchar* response_json) { - if (!response_json || strlen(response_json) == 0) + if (!response_json || strlen(response_json) == 0) { + log_warning("[AI-THREAD] Empty or NULL response JSON"); return NULL; + } - /* Try Perplexity /v1/agent format first: look for "text":"..." inside output array - * Response: {"output":[{"content":[{"text":"...","type":"output_text"}]}]} */ - const gchar* text_start = strstr(response_json, "\"text\":\""); - if (text_start) { - text_start += strlen("\"text\":\""); - const gchar* p = text_start; - while (*p) { - if (*p == '\\' && *(p + 1) == '"') { - p += 2; - continue; - } - if (*p == '"') { - gsize len = p - text_start; - gchar* result = g_new0(gchar, len + 1); - gchar* out = result; - const gchar* in = text_start; - while (in < p) { - if (*in == '\\' && *(in + 1) == '"') { - *out++ = '"'; - in += 2; - } else if (*in == '\\' && *(in + 1) == 'n') { - *out++ = '\n'; - in += 2; - } else { - *out++ = *in++; + /* Parse JSON using json-glib */ + auto_gchar gchar* error_msg = NULL; + JsonParser* parser = json_parser_new(); + if (!json_parser_load_from_data(parser, response_json, -1, &error_msg)) { + log_warning("[AI-THREAD] Failed to parse AI response JSON: %s", error_msg ? error_msg : "unknown error"); + g_object_unref(parser); + return NULL; + } + + JsonNode* root = json_parser_get_root(parser); + if (!root || !JSON_IS_OBJECT(json_node_get_object(root))) { + log_warning("[AI-THREAD] Invalid AI response JSON: root is not an object"); + g_object_unref(parser); + return NULL; + } + + JsonObject* root_obj = json_node_get_object(root); + gchar* content = NULL; + + /* Try 1: Perplexity /v1/agent format + * {"output":[{"content":[{"text":"...","type":"output_text"}]}]} */ + JsonNode* output_node = json_object_get_member(root_obj, "output"); + if (output_node && JSON_IS_ARRAY(json_node_get_array(output_node))) { + JsonArray* output_arr = json_node_get_array(output_node); + if (json_array_get_length(output_arr) > 0) { + JsonObject* first_output = json_array_get_object_element(output_arr, 0); + JsonNode* content_node = json_object_get_member(first_output, "content"); + if (content_node && JSON_IS_ARRAY(json_node_get_array(content_node))) { + JsonArray* content_arr = json_node_get_array(content_node); + if (json_array_get_length(content_arr) > 0) { + JsonObject* first_content = json_array_get_object_element(content_arr, 0); + JsonNode* text_node = json_object_get_member(first_content, "text"); + if (text_node && JSON_IS_VALUE(text_node)) { + content = g_strdup(json_node_get_string(text_node)); } } - *out = '\0'; - return result; } - p++; } } - /* Try legacy OpenAI format: "content":"..." - * Response: {"choices":[{"message":{"content":"..."}}]} */ - const gchar* content_start = strstr(response_json, "\"content\":\""); - if (!content_start) - return NULL; - - content_start += strlen("\"content\":\""); - - /* Find the closing quote, accounting for escaped quotes */ - const gchar* p = content_start; - while (*p) { - if (*p == '\\' && *(p + 1) == '"') { - /* Escaped quote, skip both characters */ - p += 2; - continue; - } - if (*p == '"') { - /* Found unescaped closing quote */ - gsize len = p - content_start; - /* Unescape the content: convert \" back to " */ - gchar* result = g_new0(gchar, len + 1); - gchar* out = result; - const gchar* in = content_start; - while (in < p) { - if (*in == '\\' && *(in + 1) == '"') { - *out++ = '"'; - in += 2; - } else if (*in == '\\' && *(in + 1) == 'n') { - *out++ = '\n'; - in += 2; - } else { - *out++ = *in++; + /* Try 2: OpenAI /v1/chat/completions format + * {"choices":[{"message":{"content":"..."}}]} */ + if (!content) { + JsonNode* choices_node = json_object_get_member(root_obj, "choices"); + if (choices_node && JSON_IS_ARRAY(json_node_get_array(choices_node))) { + JsonArray* choices_arr = json_node_get_array(choices_node); + if (json_array_get_length(choices_arr) > 0) { + JsonObject* first_choice = json_array_get_object_element(choices_arr, 0); + JsonNode* message_node = json_object_get_member(first_choice, "message"); + if (message_node && JSON_IS_OBJECT(json_node_get_object(message_node))) { + JsonObject* message = json_node_get_object(message_node); + JsonNode* content_member = json_object_get_member(message, "content"); + if (content_member && JSON_IS_VALUE(content_member)) { + content = g_strdup(json_node_get_string(content_member)); + } } } - *out = '\0'; - return result; } - p++; } - return NULL; + /* Try 3: Fallback - look for "text" field anywhere in the response + * This handles edge cases where the response shape differs */ + if (!content) { + JsonNode* text_node = json_object_get_member(root_obj, "text"); + if (text_node && JSON_IS_VALUE(text_node)) { + content = g_strdup(json_node_get_string(text_node)); + } + } + + /* Try 4: Fallback - look for "content" field at root level */ + if (!content) { + JsonNode* content_node = json_object_get_member(root_obj, "content"); + if (content_node && JSON_IS_VALUE(content_node)) { + content = g_strdup(json_node_get_string(content_node)); + } + } + + g_object_unref(parser); + + if (!content) { + log_warning("[AI-THREAD] Could not extract content from AI response"); + } + + return content; } static gpointer