Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.

Commit 306aa07

Browse files
committed
Sun Apr 21 13:27:14 PDT 2024
1 parent d90aae5 commit 306aa07

13 files changed

Lines changed: 273 additions & 55 deletions

File tree

.vscode/launch.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "bun",
9+
"internalConsoleOptions": "neverOpen",
10+
"request": "launch",
11+
"name": "Debug File",
12+
"program": "${file}",
13+
"cwd": "${workspaceFolder}",
14+
"stopOnEntry": false,
15+
"watchMode": false
16+
},
17+
{
18+
"type": "bun",
19+
"internalConsoleOptions": "neverOpen",
20+
"request": "attach",
21+
"name": "Attach Bun",
22+
"url": "ws://localhost:6499/",
23+
"stopOnEntry": false
24+
}
25+
]
26+
}

ollama/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Changelog is available here: https://github.com/ollama/ollama/releases

packages/lang/assistant/index.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { DynamicStructuredTool, DynamicTool, type ToolInterface } from "@langchain/core/tools";
22
import { createAgent } from "../agent";
33
import { AgentExecutor } from "langchain/agents";
4-
import { js2xml, type ElementCompact } from "xml-js";
4+
import { js2xml, xml2js, type ElementCompact } from "xml-js";
55

6-
export const createAssistant = async (tools: ToolInterface[]) => {
6+
export const createAssistant = async (tools: ToolInterface[]): Promise<AgentExecutor> => {
77
const agent = await createAgent(tools)
88

99
return new AgentExecutor({
@@ -12,7 +12,7 @@ export const createAssistant = async (tools: ToolInterface[]) => {
1212
})
1313
}
1414

15-
export const formatAnswer = (answer: ElementCompact) => {
15+
export const formatToolAnswer = (answer: ElementCompact) => {
1616
return js2xml(answer, {
1717
compact: true,
1818
ignoreCdata: true,
@@ -21,5 +21,31 @@ export const formatAnswer = (answer: ElementCompact) => {
2121
})
2222
}
2323

24+
export const parseXmlLikeResponse = (input: string) => {
25+
try {
26+
const data: ElementCompact = xml2js(`<root>${input}</root>`, {
27+
compact: true,
28+
ignoreCdata: true,
29+
ignoreAttributes: false,
30+
ignoreInstruction: true,
31+
})
32+
33+
const message = Array.isArray(data.root._text) ?
34+
data.root._text.join(' ') :
35+
data.root._text
36+
37+
return {
38+
message,
39+
data: data.root
40+
}
41+
} catch (error) {
42+
console.warn('failed to parse xml-like response', input, error)
43+
return {
44+
message: input,
45+
data: { _text: input }
46+
}
47+
}
48+
}
49+
2450
export { DynamicStructuredTool, DynamicTool, type ToolInterface }
2551
export { AIMessage, SystemMessage, HumanMessage } from "@langchain/core/messages"

server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Changelog is available here: https://github.com/0x77dev/ava/releases

server/src/addon-config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ export const injectHassOptions = async () => {
2121
process.env.EMBEDDINGS = JSON.stringify(options.embeddings)
2222
}
2323

24-
process.env.HOMEASSISTANT = JSON.stringify({
25-
"token": process.env.SUPERVISOR_TOKEN,
26-
"url": "ws://supervisor/core/websocket",
27-
})
24+
if (process.env.SUPERVISOR_TOKEN) {
25+
process.env.HOMEASSISTANT = JSON.stringify({
26+
"token": process.env.SUPERVISOR_TOKEN,
27+
"url": "ws://supervisor:80/core/websocket",
28+
"supervisor": true
29+
})
30+
}
2831
}
2932

3033
await injectHassOptions()

server/src/services/oai/lang.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import type { ChatCompletionRequestMessage, CreateChatCompletionRequest, CreateChatCompletionResponse } from "@ava/oai-types"
1+
import type {
2+
ChatCompletionRequestMessage,
3+
CreateChatCompletionRequest,
4+
CreateChatCompletionResponse
5+
} from "@ava/oai-types"
26
import { v4 } from "uuid"
37
import { getAssistant } from "../assistant"
4-
import { AIMessage, HumanMessage, SystemMessage } from "@ava/lang"
8+
import {
9+
AIMessage,
10+
HumanMessage,
11+
SystemMessage,
12+
parseXmlLikeResponse
13+
} from "@ava/lang"
514

615
const oaiToLangMessage = (messages: ChatCompletionRequestMessage[]) => {
7-
return messages.map(({role, content}) => {
8-
if(!content) return
16+
return messages.map(({ role, content }) => {
17+
if (!content) return
918

1019
switch (role) {
1120
case 'user':
@@ -24,7 +33,7 @@ export const executeChatCompletion = async (req: CreateChatCompletionRequest): P
2433
const id = v4()
2534
const assistant = await getAssistant()
2635

27-
if(req.stream) {
36+
if (req.stream) {
2837
throw new Error('streaming is not supported yet')
2938
}
3039

@@ -35,7 +44,6 @@ export const executeChatCompletion = async (req: CreateChatCompletionRequest): P
3544
const input = lastMessage.content
3645
const chat_history = oaiToLangMessage(req.messages.slice(0, -1))
3746

38-
3947
const res = await assistant.invoke({
4048
input,
4149
chat_history,
@@ -45,15 +53,18 @@ export const executeChatCompletion = async (req: CreateChatCompletionRequest): P
4553
}
4654
})
4755

56+
const parsed = parseXmlLikeResponse(res.output)
57+
4858
return {
4959
id,
5060
model: 'ava',
5161
choices: [
5262
{
5363
index: 0,
5464
finish_reason: 'stop',
55-
message: res.output,
56-
}
65+
message: parsed.message,
66+
meta: parsed.data
67+
} as CreateChatCompletionResponse['choices'][number] & { meta: any }
5768
],
5869
object: 'chat.completion',
5970
created: Math.floor(Date.now() / 1000)

skills/homeassistant/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { parseEnv } from "znv";
1+
import { parseEnv, z } from "znv";
22
import { HomeassistantSchema } from "./schema";
33

44
export const { HOMEASSISTANT } = parseEnv(process.env, {
55
HOMEASSISTANT: {
6-
schema: HomeassistantSchema,
6+
schema: z.string().transform((value) => HomeassistantSchema.parse(JSON.parse(value))),
77
description: "Home Assistant configuration"
88
}
9-
})
9+
})

skills/homeassistant/provider.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
createConnection,
3+
createLongLivedTokenAuth,
4+
ERR_CANNOT_CONNECT,
5+
ERR_INVALID_AUTH,
6+
ERR_CONNECTION_LOST,
7+
ERR_HASS_HOST_REQUIRED,
8+
ERR_INVALID_HTTPS_TO_HTTP,
9+
ERR_INVALID_AUTH_CALLBACK,
10+
} from "home-assistant-js-websocket";
11+
import { HOMEASSISTANT } from "../config";
12+
import { createConnectionFromHassio } from "./supervisor";
13+
14+
const errorMap: Record<number, string> = {
15+
[ERR_CANNOT_CONNECT]: "ERR_CANNOT_CONNECT",
16+
[ERR_INVALID_AUTH]: "ERR_INVALID_AUTH",
17+
[ERR_CONNECTION_LOST]: "ERR_CONNECTION_LOST",
18+
[ERR_HASS_HOST_REQUIRED]: "ERR_HASS_HOST_REQUIRED",
19+
[ERR_INVALID_HTTPS_TO_HTTP]: "ERR_INVALID_HTTPS_TO_HTTP",
20+
[ERR_INVALID_AUTH_CALLBACK]: "ERR_INVALID_AUTH_CALLBACK",
21+
}
22+
23+
const createHass = async () => {
24+
try {
25+
return HOMEASSISTANT.supervisor ?
26+
await createConnectionFromHassio() :
27+
await createConnection({
28+
auth: createLongLivedTokenAuth(HOMEASSISTANT.url, HOMEASSISTANT.token)
29+
})
30+
} catch (error: any) {
31+
if (typeof error === "number") {
32+
console.error(errorMap[error])
33+
} else {
34+
console.error(error)
35+
}
36+
process.exit(1)
37+
}
38+
}
39+
40+
export const hass = await createHass()
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Connection } from "home-assistant-js-websocket"
2+
import { HOMEASSISTANT } from "../config"
3+
4+
import {
5+
ERR_INVALID_AUTH,
6+
ERR_CANNOT_CONNECT,
7+
} from "home-assistant-js-websocket/dist/errors.js";
8+
import type { Error } from "home-assistant-js-websocket";
9+
import * as messages from "home-assistant-js-websocket/dist/messages.js";
10+
import { atLeastHaVersion } from "home-assistant-js-websocket/dist/util.js";
11+
12+
const DEBUG = true;
13+
14+
export const MSG_TYPE_AUTH_REQUIRED = "auth_required";
15+
export const MSG_TYPE_AUTH_INVALID = "auth_invalid";
16+
export const MSG_TYPE_AUTH_OK = "auth_ok";
17+
18+
export interface HaWebSocket extends WebSocket {
19+
haVersion: string;
20+
}
21+
22+
const getWSUrl = () => {
23+
const url = new URL(HOMEASSISTANT.url)
24+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
25+
url.pathname = '/core/websocket'
26+
return url.toString()
27+
}
28+
29+
export function createSocket(): Promise<HaWebSocket> {
30+
// Convert from http:// -> ws://, https:// -> wss://
31+
const url = getWSUrl();
32+
33+
if (DEBUG) {
34+
console.log("[Auth phase] Initializing", url);
35+
}
36+
37+
function connect(
38+
triesLeft: number,
39+
promResolve: (socket: HaWebSocket) => void,
40+
promReject: (err: Error) => void,
41+
) {
42+
if (DEBUG) {
43+
console.log("[Auth phase] New connection", url);
44+
}
45+
46+
const socket = new WebSocket(url) as HaWebSocket;
47+
48+
// If invalid auth, we will not try to reconnect.
49+
let invalidAuth = false;
50+
51+
const closeMessage = () => {
52+
// If we are in error handler make sure close handler doesn't also fire.
53+
socket.removeEventListener("close", closeMessage);
54+
if (invalidAuth) {
55+
promReject(ERR_INVALID_AUTH);
56+
return;
57+
}
58+
59+
// Reject if we no longer have to retry
60+
if (triesLeft === 0) {
61+
// We never were connected and will not retry
62+
promReject(ERR_CANNOT_CONNECT);
63+
return;
64+
}
65+
66+
const newTries = triesLeft === -1 ? -1 : triesLeft - 1;
67+
// Try again in a second
68+
setTimeout(() => connect(newTries, promResolve, promReject), 1000);
69+
};
70+
71+
// Auth is mandatory, so we can send the auth message right away.
72+
const handleOpen = async (event: MessageEventInit) => {
73+
try {
74+
socket.send(JSON.stringify({
75+
...messages.auth(HOMEASSISTANT.token),
76+
api_password: HOMEASSISTANT.token
77+
}));
78+
} catch (err) {
79+
// Refresh token failed
80+
invalidAuth = err === ERR_INVALID_AUTH;
81+
socket.close();
82+
}
83+
};
84+
85+
const handleMessage = async (event: MessageEvent) => {
86+
const message = JSON.parse(event.data);
87+
88+
if (DEBUG) {
89+
console.log("[Auth phase] Received", message);
90+
}
91+
switch (message.type) {
92+
case MSG_TYPE_AUTH_INVALID:
93+
invalidAuth = true;
94+
socket.close();
95+
break;
96+
97+
case MSG_TYPE_AUTH_OK:
98+
socket.removeEventListener("open", handleOpen);
99+
socket.removeEventListener("message", handleMessage);
100+
socket.removeEventListener("close", closeMessage);
101+
socket.removeEventListener("error", closeMessage);
102+
socket.haVersion = message.ha_version;
103+
if (atLeastHaVersion(socket.haVersion, 2022, 9)) {
104+
socket.send(JSON.stringify(messages.supportedFeatures()));
105+
}
106+
107+
promResolve(socket);
108+
break;
109+
110+
default:
111+
if (DEBUG) {
112+
// We already send response to this message when socket opens
113+
if (message.type !== MSG_TYPE_AUTH_REQUIRED) {
114+
console.warn("[Auth phase] Unhandled message", message);
115+
}
116+
}
117+
}
118+
};
119+
120+
socket.addEventListener("open", handleOpen);
121+
socket.addEventListener("message", handleMessage);
122+
socket.addEventListener("close", closeMessage);
123+
socket.addEventListener("error", closeMessage);
124+
}
125+
126+
return new Promise((resolve, reject) =>
127+
connect(3, resolve, reject),
128+
);
129+
}
130+
131+
export const createConnectionFromHassio = async () => {
132+
return new Connection(await createSocket(), {
133+
createSocket,
134+
setupRetry: 3
135+
})
136+
}

0 commit comments

Comments
 (0)