arisa 3.0.10 → 3.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -8
- package/package.json +2 -1
- package/src/core/agent/agent-manager.js +1 -1
- package/src/core/tools/tool-registry.js +6 -4
- package/src/core/tools/tool-result.js +71 -0
- package/src/runtime/paths.js +0 -2
- package/src/transport/telegram/bot.js +15 -15
- package/tools/openai-transcribe/index.js +9 -4
- package/tools/openai-tts/index.js +15 -13
- package/tools/web-browser/index.js +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Arisa
|
|
2
2
|
|
|
3
|
-
Arisa is a personal Telegram assistant powered by Pi Agent.
|
|
3
|
+
Arisa is a personal Telegram assistant powered by [Pi Agent](https://pi.dev).
|
|
4
4
|
|
|
5
5
|
## Origin
|
|
6
6
|
|
|
@@ -64,13 +64,6 @@ That isolation is part of the architecture:
|
|
|
64
64
|
- each tool can have its own dependencies
|
|
65
65
|
- one tool can be changed or replaced without tightly coupling the rest of the system
|
|
66
66
|
|
|
67
|
-
Each tool must support:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
node index.js --help
|
|
71
|
-
node index.js run --request-file <json>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
67
|
### Configuration model
|
|
75
68
|
- all runtime state lives under `~/.arisa/`
|
|
76
69
|
- Telegram runtime config is stored in `~/.arisa/state/config.json`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.11",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"zeroclaw",
|
|
28
28
|
"jarvis",
|
|
29
29
|
"AGENTS.md",
|
|
30
|
+
"pi.dev",
|
|
30
31
|
"clasen"
|
|
31
32
|
],
|
|
32
33
|
"author": "",
|
|
@@ -123,7 +123,7 @@ export class AgentManager {
|
|
|
123
123
|
defineTool({
|
|
124
124
|
name: "run_tool",
|
|
125
125
|
label: "Run tool",
|
|
126
|
-
description: "Run a CLI tool using text input or an artifactId. If
|
|
126
|
+
description: "Run a CLI tool using text input or an artifactId. Inspect the returned status/resolution fields. If a tool reports missing config, ask the user naturally, use set_tool_config, and retry.",
|
|
127
127
|
parameters: Type.Object({
|
|
128
128
|
name: Type.String(),
|
|
129
129
|
artifactId: Type.Optional(Type.String()),
|
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getToolConfigPath, getToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
|
|
6
6
|
import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
|
|
7
|
+
import { normalizeToolResult } from "./tool-result.js";
|
|
7
8
|
|
|
8
9
|
const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
|
|
9
10
|
const toolRoots = [
|
|
@@ -117,15 +118,16 @@ export class ToolRegistry {
|
|
|
117
118
|
await unlink(requestFile).catch(() => {});
|
|
118
119
|
try {
|
|
119
120
|
const parsed = JSON.parse(result.stdout || result.stderr);
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
const normalized = normalizeToolResult(name, parsed);
|
|
122
|
+
this.logger?.log("tools", `${name} -> ${normalized.ok === false ? normalized.status || "error" : "ok"}`);
|
|
123
|
+
return normalized;
|
|
122
124
|
} catch {
|
|
123
|
-
return {
|
|
125
|
+
return normalizeToolResult(name, {
|
|
124
126
|
ok: false,
|
|
125
127
|
error: `Invalid tool response for ${name}`,
|
|
126
128
|
stdout: result.stdout,
|
|
127
129
|
stderr: result.stderr
|
|
128
|
-
};
|
|
130
|
+
});
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function toolOk(output = {}, extra = {}) {
|
|
2
|
+
return { ok: true, output, ...extra };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toolError(error, extra = {}) {
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
status: extra.status || "failed",
|
|
9
|
+
error,
|
|
10
|
+
...extra
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toolNeedsConfig({ tool, missingConfig = [], configPath, message } = {}) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
status: "needs_config",
|
|
18
|
+
error: message || `Missing tool configuration${tool ? ` for ${tool}` : ""}.`,
|
|
19
|
+
missingConfig,
|
|
20
|
+
configPath,
|
|
21
|
+
resolution: {
|
|
22
|
+
type: "user_config_required",
|
|
23
|
+
tool,
|
|
24
|
+
missingConfig,
|
|
25
|
+
configPath
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeToolResult(name, result = {}) {
|
|
31
|
+
if (!result || typeof result !== "object") {
|
|
32
|
+
return toolError(`Invalid tool response for ${name}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result.ok === false && result.missingConfig?.length) {
|
|
36
|
+
return {
|
|
37
|
+
...toolNeedsConfig({
|
|
38
|
+
tool: name,
|
|
39
|
+
missingConfig: result.missingConfig,
|
|
40
|
+
configPath: result.configPath,
|
|
41
|
+
message: result.error
|
|
42
|
+
}),
|
|
43
|
+
...result,
|
|
44
|
+
status: result.status || "needs_config",
|
|
45
|
+
resolution: result.resolution || {
|
|
46
|
+
type: "user_config_required",
|
|
47
|
+
tool: name,
|
|
48
|
+
missingConfig: result.missingConfig,
|
|
49
|
+
configPath: result.configPath
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (result.ok === false) {
|
|
55
|
+
return {
|
|
56
|
+
...toolError(result.error || `Tool failed: ${name}`),
|
|
57
|
+
...result,
|
|
58
|
+
status: result.status || "failed"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.ok === true) {
|
|
63
|
+
return {
|
|
64
|
+
...toolOk(result.output || {}),
|
|
65
|
+
...result,
|
|
66
|
+
status: result.status || "ok"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return toolError(`Invalid tool response for ${name}`, { rawResult: result });
|
|
71
|
+
}
|
package/src/runtime/paths.js
CHANGED
|
@@ -10,7 +10,6 @@ export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
|
10
10
|
export const artifactsDir = path.join(arisaHomeDir, "artifacts");
|
|
11
11
|
export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
|
|
12
12
|
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
13
|
-
export const tmpDir = path.join(arisaHomeDir, "tmp");
|
|
14
13
|
|
|
15
14
|
export function getToolDir(toolName) {
|
|
16
15
|
return path.join(toolsDir, toolName);
|
|
@@ -36,6 +35,5 @@ export async function ensureArisaHome() {
|
|
|
36
35
|
await mkdir(stateDir, { recursive: true });
|
|
37
36
|
await mkdir(artifactsDir, { recursive: true });
|
|
38
37
|
await mkdir(toolsDir, { recursive: true });
|
|
39
|
-
await mkdir(tmpDir, { recursive: true });
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -2,7 +2,7 @@ import { Bot, InputFile } from "grammy";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
5
|
-
import { renderTelegramHtml
|
|
5
|
+
import { renderTelegramHtml } from "./text-format.js";
|
|
6
6
|
|
|
7
7
|
function quotedMessageSummary(message) {
|
|
8
8
|
if (!message) return [];
|
|
@@ -32,13 +32,14 @@ function quotedMessageSummary(message) {
|
|
|
32
32
|
return parts;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function buildPrompt({ ctx, artifact, transcript }) {
|
|
35
|
+
function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
36
36
|
const parts = [
|
|
37
37
|
`New Telegram message.`,
|
|
38
38
|
`chatId: ${ctx.chat.id}`,
|
|
39
39
|
`userId: ${ctx.from.id}`,
|
|
40
40
|
`username: ${ctx.from.username || "(no username)"}`,
|
|
41
|
-
`messageId: ${ctx.msg.message_id}
|
|
41
|
+
`messageId: ${ctx.msg.message_id}`,
|
|
42
|
+
`preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
|
|
@@ -52,6 +53,10 @@ function buildPrompt({ ctx, artifact, transcript }) {
|
|
|
52
53
|
parts.push(`transcriptText: ${transcript.text}`);
|
|
53
54
|
parts.push(`Important: the incoming audio has already been transcribed. Use the transcript as the user message content. Do not answer with a raw transcription unless the user explicitly asked for one.`);
|
|
54
55
|
}
|
|
56
|
+
if (artifact?.kind === "audio" && !transcript && toolResult) {
|
|
57
|
+
parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
58
|
+
parts.push(`Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this voice/audio message.`);
|
|
59
|
+
}
|
|
55
60
|
|
|
56
61
|
parts.push(`If you need a CLI tool, use list_tools/tool_help/run_tool.`);
|
|
57
62
|
parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
|
|
@@ -75,7 +80,7 @@ async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactSt
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
if (!result.output?.text) {
|
|
78
|
-
return { transcript: null, toolResult: { ok: false, error: "Transcription returned no text." } };
|
|
83
|
+
return { transcript: null, toolResult: { ok: false, status: "failed", error: "Transcription returned no text." } };
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
const transcript = await artifactStore.createText({
|
|
@@ -139,18 +144,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
139
144
|
const { transcript, toolResult } = await maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore });
|
|
140
145
|
if (transcript) logger?.log("telegram", `audio transcribed to artifact ${transcript.id}`);
|
|
141
146
|
if (artifact?.kind === "audio" && !transcript) {
|
|
142
|
-
|
|
143
|
-
throw new Error("I need the OpenAI API key for ~/.arisa/tools/openai-transcribe/config.js before I can transcribe incoming audio.");
|
|
144
|
-
}
|
|
145
|
-
throw new Error(toolResult?.error || "Audio transcription failed.");
|
|
147
|
+
logger?.log("telegram", `audio normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
|
|
146
148
|
}
|
|
147
|
-
return buildPrompt({ ctx, artifact, transcript });
|
|
149
|
+
return buildPrompt({ ctx, artifact, transcript, toolResult });
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
async function sendTextReply({ sendText, sendDocument, chatId, text }) {
|
|
151
|
-
const
|
|
153
|
+
const maxInlineReplyLength = 3500;
|
|
152
154
|
|
|
153
|
-
if (text.length >
|
|
155
|
+
if (text.length > maxInlineReplyLength) {
|
|
154
156
|
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
155
157
|
const artifact = await artifactStore.createGeneratedFile({
|
|
156
158
|
fileName: `reply-${Date.now()}.md`,
|
|
@@ -167,9 +169,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
logger?.log("telegram", `sending text reply for chat ${chatId}`);
|
|
170
|
-
|
|
171
|
-
await sendText(renderTelegramHtml(chunk), { parse_mode: "HTML" });
|
|
172
|
-
}
|
|
172
|
+
await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
async function processPrompt(ctx, prompt) {
|
|
@@ -250,7 +250,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
250
250
|
const chatState = getChatState(ctx.chat.id);
|
|
251
251
|
chatState.processing = false;
|
|
252
252
|
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
-
await ctx.reply(
|
|
253
|
+
await ctx.reply(message);
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
256
|
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import defaults from "./config.js";
|
|
4
4
|
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
|
+
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
5
6
|
import { getToolConfigPath } from "../../src/runtime/paths.js";
|
|
6
7
|
|
|
7
8
|
const toolName = "openai-transcribe";
|
|
@@ -13,14 +14,18 @@ function printHelp() {
|
|
|
13
14
|
|
|
14
15
|
async function run(requestFile) {
|
|
15
16
|
if (!config.OPENAI_API_KEY) {
|
|
16
|
-
console.log(JSON.stringify({
|
|
17
|
+
console.log(JSON.stringify(toolNeedsConfig({
|
|
18
|
+
tool: toolName,
|
|
19
|
+
missingConfig: ["OPENAI_API_KEY"],
|
|
20
|
+
configPath: getToolConfigPath(toolName)
|
|
21
|
+
})));
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
21
26
|
const artifact = request.artifact;
|
|
22
27
|
if (!artifact?.path) {
|
|
23
|
-
console.log(JSON.stringify(
|
|
28
|
+
console.log(JSON.stringify(toolError("artifact.path is required")));
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -38,11 +43,11 @@ async function run(requestFile) {
|
|
|
38
43
|
|
|
39
44
|
const payload = await response.json();
|
|
40
45
|
if (!response.ok) {
|
|
41
|
-
console.log(JSON.stringify(
|
|
46
|
+
console.log(JSON.stringify(toolError(payload.error?.message || "OpenAI transcription failed")));
|
|
42
47
|
return;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
console.log(JSON.stringify({
|
|
50
|
+
console.log(JSON.stringify(toolOk({ text: payload.text || "" })));
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const args = process.argv.slice(2);
|
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import defaults from "./config.js";
|
|
4
4
|
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
|
+
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
5
6
|
import { getToolConfigPath, getToolOutDir } from "../../src/runtime/paths.js";
|
|
6
7
|
|
|
7
8
|
const toolName = "openai-tts";
|
|
@@ -13,14 +14,18 @@ function printHelp() {
|
|
|
13
14
|
|
|
14
15
|
async function run(requestFile) {
|
|
15
16
|
if (!config.OPENAI_API_KEY) {
|
|
16
|
-
console.log(JSON.stringify({
|
|
17
|
+
console.log(JSON.stringify(toolNeedsConfig({
|
|
18
|
+
tool: toolName,
|
|
19
|
+
missingConfig: ["OPENAI_API_KEY"],
|
|
20
|
+
configPath: getToolConfigPath(toolName)
|
|
21
|
+
})));
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
21
26
|
const inputText = request.text || request.artifact?.text;
|
|
22
27
|
if (!inputText) {
|
|
23
|
-
console.log(JSON.stringify(
|
|
28
|
+
console.log(JSON.stringify(toolError("text or artifact.text is required")));
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -40,7 +45,7 @@ async function run(requestFile) {
|
|
|
40
45
|
|
|
41
46
|
if (!response.ok) {
|
|
42
47
|
const payload = await response.text();
|
|
43
|
-
console.log(JSON.stringify(
|
|
48
|
+
console.log(JSON.stringify(toolError(payload)));
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -49,16 +54,13 @@ async function run(requestFile) {
|
|
|
49
54
|
const filePath = path.join(outDir, `speech-${Date.now()}.ogg`);
|
|
50
55
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
51
56
|
await writeFile(filePath, buffer);
|
|
52
|
-
console.log(JSON.stringify({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
delivery: { method: "voice" }
|
|
60
|
-
}
|
|
61
|
-
}));
|
|
57
|
+
console.log(JSON.stringify(toolOk({
|
|
58
|
+
filePath,
|
|
59
|
+
fileName: path.basename(filePath),
|
|
60
|
+
mimeType: "audio/ogg",
|
|
61
|
+
kind: "audio",
|
|
62
|
+
delivery: { method: "voice" }
|
|
63
|
+
})));
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
const args = process.argv.slice(2);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
|
|
2
3
|
|
|
3
4
|
function printHelp() {
|
|
4
5
|
console.log(`web-browser\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "weather toronto" | "https://example.com",\n "artifact": { "text": "weather toronto" },\n "args": {\n "mode": "search" | "open",\n "url": "https://example.com",\n "maxResults": "5"\n }\n }\n\nBehavior:\n - If the input looks like a URL, open the page.\n - Otherwise, perform a web search.\n - When possible, opening pages uses r.jina.ai with a direct fetch fallback.\n`);
|
|
@@ -121,7 +122,7 @@ async function run(requestFile) {
|
|
|
121
122
|
const maxResults = Number.parseInt(request.args?.maxResults || "5", 10);
|
|
122
123
|
|
|
123
124
|
if (!rawInput.trim()) {
|
|
124
|
-
console.log(JSON.stringify(
|
|
125
|
+
console.log(JSON.stringify(toolError("text, artifact.text, or args.url is required")));
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -129,9 +130,9 @@ async function run(requestFile) {
|
|
|
129
130
|
const outputText = mode === "open"
|
|
130
131
|
? await openWebPage(rawInput)
|
|
131
132
|
: await searchWeb(rawInput, Number.isFinite(maxResults) ? maxResults : 5);
|
|
132
|
-
console.log(JSON.stringify({
|
|
133
|
+
console.log(JSON.stringify(toolOk({ text: outputText })));
|
|
133
134
|
} catch (error) {
|
|
134
|
-
console.log(JSON.stringify(
|
|
135
|
+
console.log(JSON.stringify(toolError(error.message || String(error))));
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|