feat(ai): add hello world proxy client
This commit is contained in:
parent
d4dab69573
commit
dc09639cb1
114
apis/libaihelloworld.lua
Normal file
114
apis/libaihelloworld.lua
Normal 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;
|
||||||
46
programs/ai-helloworld.lua
Normal file
46
programs/ai-helloworld.lua
Normal 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
168
tests/ai-helloworld.lua
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user