fix(ai): use async prompts by default
This commit is contained in:
parent
3b647090fa
commit
3cea167261
141
apis/libai.lua
141
apis/libai.lua
@ -10,65 +10,12 @@ local DEFAULT_LUA_EXEC_TIMEOUT_SECONDS = 5;
|
||||
local DEFAULT_SESSION_SETTING_KEY = 'opencc.session_id';
|
||||
local DEFAULT_AGENT_SETTING_KEY = 'opencc.agent';
|
||||
|
||||
local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
local function base64encode(s)
|
||||
local pad = (3 - #s % 3) % 3;
|
||||
s = s .. string.rep('\0', pad);
|
||||
local r = {};
|
||||
for i = 1, #s, 3 do
|
||||
local a, b, c = s:byte(i), s:byte(i + 1), s:byte(i + 2);
|
||||
local n = a * 65536 + b * 256 + c;
|
||||
r[#r + 1] = B64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
|
||||
.. B64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
|
||||
.. B64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
|
||||
.. B64:sub(n % 64 + 1, n % 64 + 1);
|
||||
end
|
||||
local result = table.concat(r);
|
||||
if pad > 0 then
|
||||
result = result:sub(1, #result - pad) .. string.rep('=', pad);
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
local function trimTrailingSlash(s)
|
||||
return (s:gsub('/+$', ''));
|
||||
end
|
||||
|
||||
local function urlEncode(s)
|
||||
return (tostring(s):gsub('[^%w%-_%.~]', function(c)
|
||||
return string.format('%%%02X', string.byte(c));
|
||||
end));
|
||||
end
|
||||
local createHttp = require('/apis/libhttp');
|
||||
|
||||
local function isBlank(s)
|
||||
return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil;
|
||||
end
|
||||
|
||||
local function queryString(params)
|
||||
local parts = {};
|
||||
for _, item in ipairs(params) do
|
||||
if not isBlank(item[2]) then
|
||||
parts[#parts + 1] = urlEncode(item[1]) .. '=' .. urlEncode(item[2]);
|
||||
end
|
||||
end
|
||||
if #parts == 0 then return ''; end
|
||||
return '?' .. table.concat(parts, '&');
|
||||
end
|
||||
|
||||
local function readAllAndClose(response)
|
||||
local body = response.readAll();
|
||||
response.close();
|
||||
return body;
|
||||
end
|
||||
|
||||
local function statusCode(response)
|
||||
if response.getResponseCode then
|
||||
return response.getResponseCode();
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
local function extractTextParts(parts)
|
||||
if type(parts) ~= 'table' then
|
||||
return '';
|
||||
@ -210,6 +157,7 @@ local function createAi(opts)
|
||||
local eventloopFactory = opts.eventloop or require('/apis/eventloop');
|
||||
local nowFunc = opts.now or nowSeconds;
|
||||
local osLib = opts.os or os;
|
||||
local httpClient = opts.httpClient or createHttp({ http = httpLib });
|
||||
|
||||
local api = {};
|
||||
|
||||
@ -278,7 +226,7 @@ local function createAi(opts)
|
||||
local directory = options.directory or settingsLib.get('opencc.directory');
|
||||
local providerId, modelId = resolveModel(options);
|
||||
return {
|
||||
url = trimTrailingSlash(url),
|
||||
url = httpClient.trimTrailingSlash(url),
|
||||
username = username,
|
||||
password = password,
|
||||
directory = directory,
|
||||
@ -333,6 +281,29 @@ local function createAi(opts)
|
||||
return type(message.info.time) == 'table' and message.info.time.completed ~= nil;
|
||||
end
|
||||
|
||||
local function errorMessage(errorInfo)
|
||||
if type(errorInfo) ~= 'table' then return nil; end
|
||||
if type(errorInfo.data) == 'table' and type(errorInfo.data.message) == 'string' then
|
||||
return errorInfo.data.message;
|
||||
end
|
||||
if type(errorInfo.message) == 'string' then
|
||||
return errorInfo.message;
|
||||
end
|
||||
if type(errorInfo.name) == 'string' then
|
||||
return errorInfo.name;
|
||||
end
|
||||
return 'unknown assistant error';
|
||||
end
|
||||
|
||||
local function sessionStatusText(status)
|
||||
if type(status) ~= 'table' then return nil; end
|
||||
if type(status.type) ~= 'string' then return nil; end
|
||||
if status.type == 'retry' then
|
||||
return 'retry #' .. tostring(status.attempt or '?') .. ': ' .. tostring(status.message or 'unknown error');
|
||||
end
|
||||
return status.type;
|
||||
end
|
||||
|
||||
local function decodeMessage(value)
|
||||
local decoded = value;
|
||||
if type(value) == 'string' then
|
||||
@ -409,16 +380,30 @@ local function createAi(opts)
|
||||
local reply = decoded and extractTextParts(decoded.parts) or '';
|
||||
local complete = decoded and isMessageComplete(decoded) or false;
|
||||
local matchedId = decoded and type(decoded.info) == 'table' and decoded.info.id or 'nil';
|
||||
local assistantError = decoded and type(decoded.info) == 'table' and errorMessage(decoded.info.error) or nil;
|
||||
log('poll #' .. tostring(attemptCount)
|
||||
.. ': messages=' .. tostring(#messages)
|
||||
.. ', found=' .. tostring(matchedId)
|
||||
.. ', complete=' .. tostring(complete)
|
||||
.. ', text=' .. tostring(reply ~= ''));
|
||||
.. ', text=' .. tostring(reply ~= '')
|
||||
.. ', error=' .. tostring(assistantError ~= nil));
|
||||
if assistantError then
|
||||
return finish(false, 'erreur assistant: ' .. assistantError);
|
||||
end
|
||||
if decoded and reply ~= '' and complete then
|
||||
log('async reply completed');
|
||||
return finish(true, { reply = reply, sessionId = sessionId, messageId = messageId });
|
||||
end
|
||||
if nowFunc() >= deadline then
|
||||
local statusBody, statusCodeValue = doGet(cfg, '/session/status');
|
||||
if statusBody and (not statusCodeValue or statusCodeValue == 200) then
|
||||
local statuses = textutils.unserializeJSON(statusBody);
|
||||
local statusText = type(statuses) == 'table' and sessionStatusText(statuses[sessionId]) or nil;
|
||||
if statusText then
|
||||
log('session status at timeout: ' .. statusText);
|
||||
return finish(false, 'delai depasse en attendant la reponse AI (status: ' .. statusText .. ')');
|
||||
end
|
||||
end
|
||||
return finish(false, 'delai depasse en attendant la reponse AI');
|
||||
end
|
||||
loop.setTimeout(attempt, cfg.pollIntervalSeconds);
|
||||
@ -429,46 +414,12 @@ local function createAi(opts)
|
||||
return resultOk, resultValue;
|
||||
end
|
||||
|
||||
local function buildHeaders(cfg)
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
['Accept'] = 'application/json',
|
||||
};
|
||||
if cfg.password and cfg.password ~= '' then
|
||||
headers['Authorization'] = 'Basic ' .. base64encode(cfg.username .. ':' .. cfg.password);
|
||||
end
|
||||
return headers;
|
||||
end
|
||||
|
||||
local function callHttp(method, request)
|
||||
local ok, response, httpErr, errorResponse = pcall(httpLib[method], request);
|
||||
if not ok then
|
||||
return nil, 'http ' .. method .. ' threw: ' .. tostring(response);
|
||||
end
|
||||
response = response or errorResponse;
|
||||
if not response then
|
||||
return nil, 'serveur injoignable: ' .. tostring(httpErr or 'unknown error');
|
||||
end
|
||||
local code = statusCode(response);
|
||||
local body = readAllAndClose(response);
|
||||
return body, code;
|
||||
end
|
||||
|
||||
function doGet(cfg, path)
|
||||
return callHttp('get', {
|
||||
url = cfg.url .. path,
|
||||
headers = buildHeaders(cfg),
|
||||
timeout = cfg.timeoutSeconds,
|
||||
});
|
||||
return httpClient.getJson(cfg, path);
|
||||
end
|
||||
|
||||
function doPost(cfg, path, payload)
|
||||
return callHttp('post', {
|
||||
url = cfg.url .. path,
|
||||
body = textutils.serializeJSON(payload),
|
||||
headers = buildHeaders(cfg),
|
||||
timeout = cfg.timeoutSeconds,
|
||||
});
|
||||
return httpClient.postJson(cfg, path, payload);
|
||||
end
|
||||
|
||||
local function askBlocking(cfg, sessionId, prompt, persist, sessionSettingKey, log)
|
||||
@ -494,7 +445,7 @@ local function createAi(opts)
|
||||
end
|
||||
|
||||
local function listSessionsWithDirectory(cfg, directory)
|
||||
return doGet(cfg, '/session' .. queryString({ { 'directory', directory } }));
|
||||
return doGet(cfg, '/session' .. httpClient.queryString({ { 'directory', directory } }));
|
||||
end
|
||||
|
||||
local function decodeSessionList(body, log)
|
||||
@ -609,8 +560,8 @@ local function createAi(opts)
|
||||
promptWithContext = buildPromptWithCallerContext(prompt, osLib);
|
||||
end
|
||||
|
||||
if not (cfg.providerID and cfg.modelID) then
|
||||
log('provider/model unset; using blocking message endpoint');
|
||||
if options.blocking == true then
|
||||
log('using blocking message endpoint');
|
||||
return askBlocking(cfg, sessionId, promptWithContext, persist, sessionSettingKey, log);
|
||||
end
|
||||
|
||||
|
||||
122
apis/libhttp.lua
Normal file
122
apis/libhttp.lua
Normal file
@ -0,0 +1,122 @@
|
||||
local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
local function base64encode(s)
|
||||
local pad = (3 - #s % 3) % 3;
|
||||
s = s .. string.rep('\0', pad);
|
||||
local r = {};
|
||||
for i = 1, #s, 3 do
|
||||
local a, b, c = s:byte(i), s:byte(i + 1), s:byte(i + 2);
|
||||
local n = a * 65536 + b * 256 + c;
|
||||
r[#r + 1] = B64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
|
||||
.. B64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
|
||||
.. B64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
|
||||
.. B64:sub(n % 64 + 1, n % 64 + 1);
|
||||
end
|
||||
local result = table.concat(r);
|
||||
if pad > 0 then
|
||||
result = result:sub(1, #result - pad) .. string.rep('=', pad);
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
local function trimTrailingSlash(s)
|
||||
return (s:gsub('/+$', ''));
|
||||
end
|
||||
|
||||
local function urlEncode(s)
|
||||
return (tostring(s):gsub('[^%w%-_%.~]', function(c)
|
||||
return string.format('%%%02X', string.byte(c));
|
||||
end));
|
||||
end
|
||||
|
||||
local function isBlank(s)
|
||||
return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil;
|
||||
end
|
||||
|
||||
local function queryString(params)
|
||||
local parts = {};
|
||||
for _, item in ipairs(params) do
|
||||
if not isBlank(item[2]) then
|
||||
parts[#parts + 1] = urlEncode(item[1]) .. '=' .. urlEncode(item[2]);
|
||||
end
|
||||
end
|
||||
if #parts == 0 then return ''; end
|
||||
return '?' .. table.concat(parts, '&');
|
||||
end
|
||||
|
||||
local function readAllAndClose(response)
|
||||
local body = response.readAll();
|
||||
response.close();
|
||||
return body;
|
||||
end
|
||||
|
||||
local function statusCode(response)
|
||||
if response.getResponseCode then
|
||||
return response.getResponseCode();
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
local function createHttp(opts)
|
||||
opts = opts or {};
|
||||
local httpLib = opts.http or http;
|
||||
local textutilsLib = opts.textutils or textutils;
|
||||
|
||||
local api = {
|
||||
base64encode = base64encode,
|
||||
trimTrailingSlash = trimTrailingSlash,
|
||||
urlEncode = urlEncode,
|
||||
queryString = queryString,
|
||||
};
|
||||
|
||||
function api.basicAuth(username, password)
|
||||
return 'Basic ' .. base64encode(tostring(username or '') .. ':' .. tostring(password or ''));
|
||||
end
|
||||
|
||||
function api.jsonHeaders(options)
|
||||
options = options or {};
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
['Accept'] = 'application/json',
|
||||
};
|
||||
if options.password and options.password ~= '' then
|
||||
headers['Authorization'] = api.basicAuth(options.username, options.password);
|
||||
end
|
||||
return headers;
|
||||
end
|
||||
|
||||
function api.call(method, request)
|
||||
local ok, response, httpErr, errorResponse = pcall(httpLib[method], request);
|
||||
if not ok then
|
||||
return nil, 'http ' .. method .. ' threw: ' .. tostring(response);
|
||||
end
|
||||
response = response or errorResponse;
|
||||
if not response then
|
||||
return nil, 'serveur injoignable: ' .. tostring(httpErr or 'unknown error');
|
||||
end
|
||||
local code = statusCode(response);
|
||||
local body = readAllAndClose(response);
|
||||
return body, code;
|
||||
end
|
||||
|
||||
function api.getJson(cfg, path)
|
||||
return api.call('get', {
|
||||
url = cfg.url .. path,
|
||||
headers = api.jsonHeaders(cfg),
|
||||
timeout = cfg.timeoutSeconds,
|
||||
});
|
||||
end
|
||||
|
||||
function api.postJson(cfg, path, payload)
|
||||
return api.call('post', {
|
||||
url = cfg.url .. path,
|
||||
body = textutilsLib.serializeJSON(payload),
|
||||
headers = api.jsonHeaders(cfg),
|
||||
timeout = cfg.timeoutSeconds,
|
||||
});
|
||||
end
|
||||
|
||||
return api;
|
||||
end
|
||||
|
||||
return createHttp;
|
||||
@ -144,7 +144,7 @@ Abort a running generation.
|
||||
|
||||
### `POST /session/:id/prompt_async`
|
||||
|
||||
Fire-and-forget variant. Returns `204` immediately. Include `messageID` in the request body so the submitted message can be matched to the later assistant response. Opencode validates caller-provided message IDs; use IDs starting with `msg`.
|
||||
Fire-and-forget variant. Returns `204` immediately and starts generation in the background. Include `messageID` in the request body so the submitted user message can be matched to the later assistant response. Opencode validates caller-provided message IDs; use IDs starting with `msg`.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
@ -158,9 +158,9 @@ Fire-and-forget variant. Returns `204` immediately. Include `messageID` in the r
|
||||
}
|
||||
```
|
||||
|
||||
Unlike `/message`, `model` is **not** optional in practice — omitting it causes the request to be accepted (`204`) without triggering generation, so the assistant message never appears. `ai` only uses this async endpoint when `opencc.provider_id` and `opencc.model_id` are configured; otherwise it falls back to blocking `POST /session/:id/message`.
|
||||
`agent` and `model` are optional. Omit `model` to use the server/session default model, or include it to force a specific provider/model for this prompt.
|
||||
|
||||
When async mode is available, `ai` uses this endpoint to avoid `504` failures from the blocking `/message` endpoint when the LLM takes longer than one HTTP request timeout. The submitted `messageID` identifies the user or assistant message depending on the opencode response shape; `ai` polls `GET /session/:id/message` and reads the completed assistant message. If `opencc.agent` or `--agent <name>` is set, `ai` includes it as `agent` in the request body.
|
||||
`ai` uses this endpoint by default to avoid `504` failures from the blocking `/message` endpoint when the LLM takes longer than one HTTP request timeout. The submitted `messageID` identifies the user message; `ai` polls `GET /session/:id/message` and reads the completed assistant message after it. If `opencc.agent` or `--agent <name>` is set, `ai` includes it as `agent` in the request body. If generation fails in the background, opencode records the failure on the assistant message or session event stream; `ai` surfaces assistant message errors while polling.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@ Optional — scope `ai sessions` to a specific opencode project directory. If om
|
||||
set opencc.directory /Users/garm/trap/cc-libs
|
||||
```
|
||||
|
||||
Optional but recommended: pick the provider and model. When both are set, `ai` posts to `/session/:id/prompt_async` and polls for completion. Without them, `ai` falls back to blocking `/session/:id/message`, which can use the server default model but is more exposed to HTTP timeouts:
|
||||
Optional: pick the provider and model for requests from this computer. If omitted, `ai` still posts to `/session/:id/prompt_async`; opencode uses the server/session default model:
|
||||
|
||||
```sh
|
||||
set opencc.provider_id anthropic
|
||||
@ -169,6 +169,7 @@ Set settings inside the harness before running, or inject them via the test API.
|
||||
| `erreur message: HTTP 401` | Wrong password | Check `opencc.password` matches `OPENCODE_SERVER_PASSWORD` |
|
||||
| `missing prompt` | No prompt was passed | Run `ai <prompt>` or `ai ping` |
|
||||
| `session introuvable; lance: ai new <prompt>` | Session was deleted or server restarted | Run `ai new <prompt>` |
|
||||
| `erreur message: HTTP 504` | AI took too long | Retry; consider a faster model |
|
||||
| `delai depasse en attendant la reponse AI` | Async polling timed out | Increase `opencc.poll_timeout_seconds` or check opencode logs |
|
||||
| `erreur message: HTTP 504` | Blocking mode AI call took too long | Retry; prefer the default async mode |
|
||||
| `erreur assistant: ...` | Opencode accepted the async prompt but generation failed | Check the provider/model/agent and opencode logs |
|
||||
| `delai depasse en attendant la reponse AI` | Async polling timed out | Increase `opencc.poll_timeout_seconds`; use `ai --verbose` and check opencode logs |
|
||||
| `reponse vide` | Reply had no text parts | Check opencode logs |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.8.14",
|
||||
"version": "0.8.15",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
"trapos-boot": "0.3.2",
|
||||
"trapos-net": "0.3.0",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.6.12",
|
||||
"trapos-ai": "0.6.13",
|
||||
"trapos-sandbox": "0.2.2",
|
||||
"trapos": "0.8.14"
|
||||
"trapos": "0.8.15"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "trapos-ai",
|
||||
"version": "0.6.12",
|
||||
"version": "0.6.13",
|
||||
"description": "TrapOS AI client for opencode serve",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
"apis/libai.lua",
|
||||
"apis/libhttp.lua",
|
||||
"apis/libtrapgpt.lua",
|
||||
"programs/ai.lua",
|
||||
"programs/trapgpt.lua"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.8.14",
|
||||
"version": "0.8.15",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": [
|
||||
"trapos-boot",
|
||||
|
||||
67
tests/ai.lua
67
tests/ai.lua
@ -161,6 +161,18 @@ local function assistantMessage(id, reply, completed)
|
||||
};
|
||||
end
|
||||
|
||||
local function assistantErrorMessage(id, message)
|
||||
return {
|
||||
info = {
|
||||
id = id,
|
||||
role = 'assistant',
|
||||
error = { name = 'UnknownError', data = { message = message } },
|
||||
time = { completed = 1 },
|
||||
},
|
||||
parts = {},
|
||||
};
|
||||
end
|
||||
|
||||
local function postedText(call)
|
||||
local body = textutils.unserializeJSON(call.body);
|
||||
return body.parts[1].text;
|
||||
@ -358,7 +370,7 @@ testlib.test('ask creates session then sends message when no session_id', functi
|
||||
testlib.assertEquals(result.sessionId, 'ses_new');
|
||||
testlib.assertEquals(#httpStub.postCalls, 2);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session', 1, true) ~= nil);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_new/message', 1, true) ~= nil);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_new/prompt_async', 1, true) ~= nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask creates cc-ai titled sessions', function()
|
||||
@ -419,7 +431,7 @@ testlib.test('ask reuses existing session_id without creating a new session', fu
|
||||
|
||||
testlib.assertTrue(ok);
|
||||
testlib.assertEquals(#httpStub.postCalls, 1);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_existing/message', 1, true) ~= nil);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_existing/prompt_async', 1, true) ~= nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask reuses custom sessionSettingKey without creating a new session', function()
|
||||
@ -438,10 +450,10 @@ testlib.test('ask reuses custom sessionSettingKey without creating a new session
|
||||
|
||||
testlib.assertTrue(ok);
|
||||
testlib.assertEquals(#httpStub.postCalls, 1);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_trapgpt/message', 1, true) ~= nil);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_trapgpt/prompt_async', 1, true) ~= nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask falls back to blocking message when model is unset', function()
|
||||
testlib.test('ask can use blocking message when explicitly requested', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ sessionResp('ses_blocking'), messageResp('reply') },
|
||||
{}
|
||||
@ -449,13 +461,29 @@ testlib.test('ask falls back to blocking message when model is unset', function(
|
||||
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
||||
local ai = createAi({ http = httpStub, settings = settingsStub });
|
||||
|
||||
local ok, result = ai.ask('hello', { blocking = true });
|
||||
|
||||
testlib.assertTrue(ok, tostring(result));
|
||||
testlib.assertEquals(result.reply, 'reply');
|
||||
testlib.assertEquals(#httpStub.postCalls, 2);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_blocking/message', 1, true) ~= nil);
|
||||
testlib.assertEquals(#httpStub.getCalls, 0);
|
||||
end);
|
||||
|
||||
testlib.test('ask uses async prompt when model is unset', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ sessionResp('ses_async'), messageResp('reply') },
|
||||
{}
|
||||
);
|
||||
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
||||
local ai = createAi({ http = httpStub, settings = settingsStub });
|
||||
|
||||
local ok, result = ai.ask('hello');
|
||||
|
||||
testlib.assertTrue(ok, tostring(result));
|
||||
testlib.assertEquals(result.reply, 'reply');
|
||||
testlib.assertEquals(#httpStub.postCalls, 2);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_blocking/message', 1, true) ~= nil);
|
||||
testlib.assertEquals(#httpStub.getCalls, 0);
|
||||
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_async/prompt_async', 1, true) ~= nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask wraps prompt with caller context', function()
|
||||
@ -834,6 +862,29 @@ testlib.test('ask polling tolerates assistant message without parts', function()
|
||||
testlib.assertEquals(#httpStub.getCalls, 2);
|
||||
end);
|
||||
|
||||
testlib.test('ask polling reports assistant errors', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ sessionResp('ses_1'), asyncResp() },
|
||||
{
|
||||
messageListResp({ userMessage('msg_1', 'hello'), assistantErrorMessage('msg_2', 'bad model') }),
|
||||
}
|
||||
);
|
||||
local settingsStub = fakeAsyncSettings();
|
||||
local elFactory = fakeEventloopFactory();
|
||||
local ai = createAi({
|
||||
http = httpStub,
|
||||
settings = settingsStub,
|
||||
now = function() return 10; end,
|
||||
eventloop = elFactory,
|
||||
});
|
||||
|
||||
local ok, err = ai.ask('hello', { messageId = 'msg_1' });
|
||||
|
||||
testlib.assertTrue(not ok);
|
||||
testlib.assertTrue(string.find(err, 'erreur assistant: bad model', 1, true) ~= nil);
|
||||
testlib.assertEquals(#httpStub.getCalls, 1);
|
||||
end);
|
||||
|
||||
testlib.test('ask polling default timeout allows ten minute replies', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ sessionResp('ses_1'), asyncResp() },
|
||||
@ -938,7 +989,7 @@ testlib.test('ask polling times out', function()
|
||||
|
||||
testlib.assertTrue(not ok);
|
||||
testlib.assertTrue(string.find(err, 'delai depasse', 1, true) ~= nil);
|
||||
testlib.assertEquals(#httpStub.getCalls, 2);
|
||||
testlib.assertEquals(#httpStub.getCalls, 3);
|
||||
testlib.assertTrue(elState.lastLoop.inspect().stopped);
|
||||
testlib.assertEquals(#elState.lastLoop.inspect().pending, 0);
|
||||
end);
|
||||
@ -977,7 +1028,7 @@ testlib.test('ask caps polling timeout at ten minutes', function()
|
||||
|
||||
testlib.assertTrue(not ok);
|
||||
testlib.assertTrue(string.find(err, 'delai depasse', 1, true) ~= nil);
|
||||
testlib.assertEquals(#httpStub.getCalls, 3);
|
||||
testlib.assertEquals(#httpStub.getCalls, 4);
|
||||
testlib.assertEquals(now, 600);
|
||||
end);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user