feat(ai): add hello world proxy client

This commit is contained in:
Guillaume ARM 2026-06-08 21:05:31 +02:00
parent d4dab69573
commit dc09639cb1
3 changed files with 328 additions and 0 deletions

114
apis/libaihelloworld.lua Normal file
View File

@ -0,0 +1,114 @@
local _VERSION = '0.1.0';
local DEFAULT_PROMPT = 'reply with exactly: pong';
local function trimTrailingSlash(value)
return (value:gsub('/+$', ''));
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 mapStatusError(code)
if code == 401 then return 'token invalide'; end
if code == 413 then return 'prompt trop long'; end
if code == 502 then return 'agent indisponible'; end
if code == 504 then return 'agent trop lent'; end
if code then return 'erreur proxy: HTTP ' .. tostring(code); end
return 'erreur proxy inconnue';
end
local function createAiHelloWorld(opts)
opts = opts or {};
local httpLib = opts.http or http;
local settingsLib = opts.settings or settings;
local prompt = opts.prompt or DEFAULT_PROMPT;
local api = {};
function api.version()
return _VERSION;
end
function api.clearSession()
settingsLib.unset('opencc.session_id');
if settingsLib.save then settingsLib.save(); end
end
function api.askHello(options)
options = options or {};
local proxyUrl = options.proxyUrl or settingsLib.get('opencc.proxy_url');
if not proxyUrl or proxyUrl == '' then
return false, 'missing opencc.proxy_url; run: settings set opencc.proxy_url <url>';
end
local token = options.proxyToken or settingsLib.get('opencc.proxy_token');
if not token or token == '' then
return false, 'missing opencc.proxy_token; run: settings set opencc.proxy_token <token>';
end
local sessionId = options.sessionId;
if sessionId == nil then
sessionId = settingsLib.get('opencc.session_id');
end
local body = { prompt = prompt };
if sessionId and sessionId ~= '' then
body.sessionId = sessionId;
end
local response = httpLib.post(
trimTrailingSlash(proxyUrl) .. '/ask',
textutils.serializeJSON(body),
{
['Authorization'] = 'Bearer ' .. token,
['Content-Type'] = 'application/json',
}
);
if not response then
return false, 'proxy injoignable. Verifie opencc.proxy_url et http.rules';
end
local code = statusCode(response);
local responseBody = readAllAndClose(response);
if code and code ~= 200 then
return false, mapStatusError(code);
end
local decoded = textutils.unserializeJSON(responseBody or '');
if type(decoded) ~= 'table' then
return false, 'reponse proxy invalide';
end
if type(decoded.reply) ~= 'string' then
return false, 'reponse proxy sans reply';
end
if type(decoded.sessionId) == 'string' and decoded.sessionId ~= '' then
settingsLib.set('opencc.session_id', decoded.sessionId);
if settingsLib.save then settingsLib.save(); end
end
return true, {
reply = decoded.reply,
sessionId = decoded.sessionId,
truncated = decoded.truncated == true,
};
end
return api;
end
return createAiHelloWorld;

View File

@ -0,0 +1,46 @@
local _VERSION = '0.1.0';
local createAiHelloWorld = require('/apis/libaihelloworld');
local args = table.pack(...);
local command = args[1];
local function printUsage()
print('ai-helloworld usage:');
print();
print(' ai-helloworld');
print(' ai-helloworld --new');
print(' ai-helloworld --version');
print(' ai-helloworld --help');
print();
print('settings required:');
print(' opencc.proxy_url');
print(' opencc.proxy_token');
end
if command == '--version' or command == '-version' or command == 'version' then
print('ai-helloworld v' .. _VERSION);
return;
end
if command == '--help' or command == '-help' or command == 'help' then
printUsage();
return;
end
local ai = createAiHelloWorld();
if command == '--new' then
ai.clearSession();
elseif command ~= nil and command ~= '' then
printUsage();
return;
end
local ok, result = ai.askHello();
if not ok then
print(result);
return;
end
print(result.reply);

168
tests/ai-helloworld.lua Normal file
View File

@ -0,0 +1,168 @@
local createLibTest = require('/apis/libtest');
local createAiHelloWorld = require('/apis/libaihelloworld');
local testlib = createLibTest({ ... });
local function fakeSettings(initial)
local values = initial or {};
local saveCount = 0;
return {
get = function(key)
return values[key];
end,
set = function(key, value)
values[key] = value;
end,
unset = function(key)
values[key] = nil;
end,
save = function()
saveCount = saveCount + 1;
end,
values = values,
saveCount = function()
return saveCount;
end,
};
end
local function response(code, body)
return {
getResponseCode = function()
return code;
end,
readAll = function()
return body;
end,
close = function() end,
};
end
local function fakeHttp(result)
local calls = {};
return {
post = function(url, body, headers)
calls[#calls + 1] = { url = url, body = body, headers = headers };
if type(result) == 'function' then
return result(url, body, headers);
end
return result;
end,
calls = calls,
};
end
testlib.test('askHello posts ping prompt and saves session id', function()
local settingsStub = fakeSettings({
['opencc.proxy_url'] = 'https://proxy.example/',
['opencc.proxy_token'] = 'secret',
});
local httpStub = fakeHttp(response(200, textutils.serializeJSON({
sessionId = 'ses_123',
reply = 'pong',
truncated = false,
})));
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok, result = ai.askHello();
testlib.assertTrue(ok, tostring(result));
testlib.assertEquals(result.reply, 'pong');
testlib.assertEquals(result.sessionId, 'ses_123');
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_123');
testlib.assertEquals(settingsStub.saveCount(), 1);
testlib.assertEquals(#httpStub.calls, 1);
local call = httpStub.calls[1];
testlib.assertEquals(call.url, 'https://proxy.example/ask');
testlib.assertEquals(call.headers['Authorization'], 'Bearer secret');
testlib.assertEquals(call.headers['Content-Type'], 'application/json');
local request = textutils.unserializeJSON(call.body);
testlib.assertEquals(request.prompt, 'reply with exactly: pong');
testlib.assertEquals(request.sessionId, nil);
end);
testlib.test('askHello includes existing session id', function()
local settingsStub = fakeSettings({
['opencc.proxy_url'] = 'https://proxy.example',
['opencc.proxy_token'] = 'secret',
['opencc.session_id'] = 'ses_old',
});
local httpStub = fakeHttp(response(200, textutils.serializeJSON({
sessionId = 'ses_old',
reply = 'pong',
})));
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok = ai.askHello();
testlib.assertTrue(ok);
local request = textutils.unserializeJSON(httpStub.calls[1].body);
testlib.assertEquals(request.sessionId, 'ses_old');
end);
testlib.test('askHello rejects missing proxy url', function()
local settingsStub = fakeSettings({ ['opencc.proxy_token'] = 'secret' });
local httpStub = fakeHttp(response(200, '{}'));
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok, err = ai.askHello();
testlib.assertTrue(not ok);
testlib.assertTrue(string.find(err, 'opencc.proxy_url', 1, true));
testlib.assertEquals(#httpStub.calls, 0);
end);
testlib.test('askHello rejects missing proxy token', function()
local settingsStub = fakeSettings({ ['opencc.proxy_url'] = 'https://proxy.example' });
local httpStub = fakeHttp(response(200, '{}'));
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok, err = ai.askHello();
testlib.assertTrue(not ok);
testlib.assertTrue(string.find(err, 'opencc.proxy_token', 1, true));
testlib.assertEquals(#httpStub.calls, 0);
end);
testlib.test('askHello maps proxy unreachable', function()
local settingsStub = fakeSettings({
['opencc.proxy_url'] = 'https://proxy.example',
['opencc.proxy_token'] = 'secret',
});
local httpStub = fakeHttp(nil);
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok, err = ai.askHello();
testlib.assertTrue(not ok);
testlib.assertTrue(string.find(err, 'proxy injoignable', 1, true));
end);
testlib.test('askHello maps 401 response', function()
local settingsStub = fakeSettings({
['opencc.proxy_url'] = 'https://proxy.example',
['opencc.proxy_token'] = 'secret',
});
local httpStub = fakeHttp(response(401, textutils.serializeJSON({ error = 'unauthorized' })));
local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub });
local ok, err = ai.askHello();
testlib.assertTrue(not ok);
testlib.assertEquals(err, 'token invalide');
end);
testlib.test('clearSession unsets persisted session id', function()
local settingsStub = fakeSettings({ ['opencc.session_id'] = 'ses_old' });
local ai = createAiHelloWorld({ http = fakeHttp(nil), settings = settingsStub });
ai.clearSession();
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
testlib.assertEquals(settingsStub.saveCount(), 1);
end);
testlib.run();