Compare commits
1 Commits
feat/ai
...
feat/ai-js
| Author | SHA1 | Date | |
|---|---|---|---|
|
063b2ccdc8
|
@@ -41,6 +41,7 @@ RUN pacman -S --needed --noconfirm \
|
|||||||
python \
|
python \
|
||||||
wget \
|
wget \
|
||||||
sqlite \
|
sqlite \
|
||||||
|
json-glib \
|
||||||
valgrind \
|
valgrind \
|
||||||
gdk-pixbuf2 \
|
gdk-pixbuf2 \
|
||||||
qrencode
|
qrencode
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
python-dev-is-python3 \
|
python-dev-is-python3 \
|
||||||
valgrind \
|
valgrind \
|
||||||
libsqlite3-dev \
|
libsqlite3-dev \
|
||||||
|
libjson-glib-dev \
|
||||||
libgdk-pixbuf-2.0-dev \
|
libgdk-pixbuf-2.0-dev \
|
||||||
libqrencode-dev
|
libqrencode-dev
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ RUN dnf install -y \
|
|||||||
readline-devel \
|
readline-devel \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
sqlite-devel \
|
sqlite-devel \
|
||||||
|
json-glib-devel \
|
||||||
valgrind \
|
valgrind \
|
||||||
gdk-pixbuf2-devel \
|
gdk-pixbuf2-devel \
|
||||||
qrencode-devel
|
qrencode-devel
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ RUN zypper --non-interactive in --no-recommends \
|
|||||||
python310-devel \
|
python310-devel \
|
||||||
readline-devel \
|
readline-devel \
|
||||||
sqlite3-devel \
|
sqlite3-devel \
|
||||||
|
json-glib-devel \
|
||||||
valgrind \
|
valgrind \
|
||||||
gdk-pixbuf-devel \
|
gdk-pixbuf-devel \
|
||||||
qrencode-devel
|
qrencode-devel
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
python-dev-is-python3 \
|
python-dev-is-python3 \
|
||||||
valgrind \
|
valgrind \
|
||||||
libsqlite3-dev \
|
libsqlite3-dev \
|
||||||
|
libjson-glib-dev \
|
||||||
libgdk-pixbuf-2.0-dev \
|
libgdk-pixbuf-2.0-dev \
|
||||||
libqrencode-dev
|
libqrencode-dev
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ TESTS = tests/unittests/unittests
|
|||||||
check_PROGRAMS = tests/unittests/unittests
|
check_PROGRAMS = tests/unittests/unittests
|
||||||
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
|
tests_unittests_unittests_CPPFLAGS = -I$(srcdir)/tests
|
||||||
tests_unittests_unittests_SOURCES = $(unittest_sources)
|
tests_unittests_unittests_SOURCES = $(unittest_sources)
|
||||||
tests_unittests_unittests_LDADD = -lcmocka
|
tests_unittests_unittests_LDADD = -lcmocka $(json-glib_LIBS)
|
||||||
|
|
||||||
# Functional tests require libstabber.
|
# Functional tests require libstabber.
|
||||||
# They are only built when libstabber is available.
|
# They are only built when libstabber is available.
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [],
|
|||||||
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
|
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
|
||||||
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])])
|
[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])])
|
ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])
|
||||||
AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
|
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],
|
AS_IF([test "x$PLATFORM" = xosx],
|
||||||
[AM_CFLAGS="$AM_CFLAGS -Qunused-arguments"])
|
[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 $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\\\"\""
|
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_LDFLAGS)
|
||||||
AC_SUBST(AM_CFLAGS)
|
AC_SUBST(AM_CFLAGS)
|
||||||
|
|||||||
314
docs/json-glib-integration-plan.md
Normal file
314
docs/json-glib-integration-plan.md
Normal file
@@ -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 <json-glib/json-glib.h>` 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 `<glib.h>` 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
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
#include <json-glib/json-glib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/* Default providers */
|
/* Default providers */
|
||||||
@@ -582,87 +583,109 @@ _build_json_payload(AISession* session, const gchar* prompt)
|
|||||||
return json_payload;
|
return json_payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gchar*
|
/** @brief Parse AI response JSON and extract content string.
|
||||||
_parse_ai_response(const gchar* response_json)
|
*
|
||||||
|
* 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;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Try Perplexity /v1/agent format first: look for "text":"..." inside output array
|
/* Parse JSON using json-glib */
|
||||||
* Response: {"output":[{"content":[{"text":"...","type":"output_text"}]}]} */
|
auto_gchar gchar* error_msg = NULL;
|
||||||
const gchar* text_start = strstr(response_json, "\"text\":\"");
|
JsonParser* parser = json_parser_new();
|
||||||
if (text_start) {
|
if (!json_parser_load_from_data(parser, response_json, -1, &error_msg)) {
|
||||||
text_start += strlen("\"text\":\"");
|
log_warning("[AI-THREAD] Failed to parse AI response JSON: %s", error_msg ? error_msg : "unknown error");
|
||||||
const gchar* p = text_start;
|
g_object_unref(parser);
|
||||||
while (*p) {
|
return NULL;
|
||||||
if (*p == '\\' && *(p + 1) == '"') {
|
}
|
||||||
p += 2;
|
|
||||||
continue;
|
JsonNode* root = json_parser_get_root(parser);
|
||||||
}
|
if (!root || !JSON_IS_OBJECT(json_node_get_object(root))) {
|
||||||
if (*p == '"') {
|
log_warning("[AI-THREAD] Invalid AI response JSON: root is not an object");
|
||||||
gsize len = p - text_start;
|
g_object_unref(parser);
|
||||||
gchar* result = g_new0(gchar, len + 1);
|
return NULL;
|
||||||
gchar* out = result;
|
}
|
||||||
const gchar* in = text_start;
|
|
||||||
while (in < p) {
|
JsonObject* root_obj = json_node_get_object(root);
|
||||||
if (*in == '\\' && *(in + 1) == '"') {
|
gchar* content = NULL;
|
||||||
*out++ = '"';
|
|
||||||
in += 2;
|
/* Try 1: Perplexity /v1/agent format
|
||||||
} else if (*in == '\\' && *(in + 1) == 'n') {
|
* {"output":[{"content":[{"text":"...","type":"output_text"}]}]} */
|
||||||
*out++ = '\n';
|
JsonNode* output_node = json_object_get_member(root_obj, "output");
|
||||||
in += 2;
|
if (output_node && JSON_IS_ARRAY(json_node_get_array(output_node))) {
|
||||||
} else {
|
JsonArray* output_arr = json_node_get_array(output_node);
|
||||||
*out++ = *in++;
|
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":"..."
|
/* Try 2: OpenAI /v1/chat/completions format
|
||||||
* Response: {"choices":[{"message":{"content":"..."}}]} */
|
* {"choices":[{"message":{"content":"..."}}]} */
|
||||||
const gchar* content_start = strstr(response_json, "\"content\":\"");
|
if (!content) {
|
||||||
if (!content_start)
|
JsonNode* choices_node = json_object_get_member(root_obj, "choices");
|
||||||
return NULL;
|
if (choices_node && JSON_IS_ARRAY(json_node_get_array(choices_node))) {
|
||||||
|
JsonArray* choices_arr = json_node_get_array(choices_node);
|
||||||
content_start += strlen("\"content\":\"");
|
if (json_array_get_length(choices_arr) > 0) {
|
||||||
|
JsonObject* first_choice = json_array_get_object_element(choices_arr, 0);
|
||||||
/* Find the closing quote, accounting for escaped quotes */
|
JsonNode* message_node = json_object_get_member(first_choice, "message");
|
||||||
const gchar* p = content_start;
|
if (message_node && JSON_IS_OBJECT(json_node_get_object(message_node))) {
|
||||||
while (*p) {
|
JsonObject* message = json_node_get_object(message_node);
|
||||||
if (*p == '\\' && *(p + 1) == '"') {
|
JsonNode* content_member = json_object_get_member(message, "content");
|
||||||
/* Escaped quote, skip both characters */
|
if (content_member && JSON_IS_VALUE(content_member)) {
|
||||||
p += 2;
|
content = g_strdup(json_node_get_string(content_member));
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*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
|
static gpointer
|
||||||
|
|||||||
Reference in New Issue
Block a user