Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/backend/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface Message {
role: string;
content: string;
name?: string;
}

export interface Backend {
Expand Down
193 changes: 193 additions & 0 deletions src/lib/tools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import type {Message} from "$lib/backend/types";
import type {MethodSpec, ToolCall, ToolSpec} from "$lib/tools/types";



function toolOutput(toolName: string, content: string, prefix?: string, suffix?: string, role = 'system'): Message {
return {
role: role,
content: (prefix ? prefix : `${toolName} Tool Output: \n\n`)+content+(suffix ? suffix : '')
}
}

function calculatorTool(): ToolSpec {
const toolName = 'calculator'
const explanation = 'Solves complicated calculations with precision.'
const methods: MethodSpec[] = [
{
name: 'execute',
arguments: [{
name: 'calculation',
doc: 'The calculation to perform.'
}],
explanation: 'Performs the calculation using javascript syntax and outputs the result.',
examples: [
{
input: '1 + 1',
output: '2'
},
{
input: '1 + 1 * 2',
output: '3'
}
],
exec: async (toolCall: ToolCall) => {
return toolOutput(
toolName,
eval(toolCall.arguments[0]),
undefined,
"\n\nThis is the output of the calculation."
)
}
}
]

return {
name: toolName,
explanation: explanation,
methods: methods
}
}

function encyclopediaTool(baseURL: string, vectorSpaceId: string, token: string): ToolSpec {
const toolName = 'encyclopedia'
const explanation = 'Provides information about a topic.'
const methods: MethodSpec[] = [
{
name: 'query',
arguments: [{
name: 'topic',
doc: 'The topic you want to query for more information.'
}],
explanation: 'Queries the encyclopedia for a topic. It produces up to 5 results. Its output is in JSON.',
examples: [
{
input: '"Constantine III"',
output: '[\n{id: 789, similarity: 0.9, attributes: {"text": "Constantine III (Latin: Flavius Claudius Constantinus; died shortly before 18 September 411) was a common Roman soldier who was declared emperor in Roman Britain in 407 and established himself in Gaul. He was recognised as co-emperor of the Roman Empire from 409 until 411.", "title": "Constantine III (Western Roman emperor)", "url": "https://en.wikipedia.org/wiki/Constantine_III_(Western_Roman_emperor)"}}\n]'
},
{
input: '"Illuminati"',
output: `[\n{id: 123, similarity: 0.9, attributes: {"text": "The Illuminati (plural of Latin illuminatus, 'enlightened') is a name given to several groups, both real and fictitious. Historically, the name usually refers to the Bavarian Illuminati, an Enlightenment-era secret society founded on 1 May 1776 in Bavaria, today part of Germany. The society's goals were to oppose superstition, obscurantism, religious influence over public life, and abuses of state power.", "url": "https://en.wikipedia.org/wiki/Illuminati"}}\n]`
}
],
exec: async (toolCall: ToolCall) => {
const query = toolCall.arguments[0]

const data = new FormData();
data.append('vector_space_id', vectorSpaceId)
data.append('modality', 'TEXT')
data.append('top_k', '5')
data.append('query', new File([new Blob([query])], '_'))

const result = await fetch(`${baseURL}/api/v0/space/${vectorSpaceId}/lookup`, {
method: 'POST',
body: data,
headers: {
'Authorization': 'Bearer ' + token
}
})
const json = await result.json()

const results = json.results


const output = codeBlock(
JSON.stringify(results, (k, v) => v.toFixed ? Number(v.toFixed(3)) : v
), 'json');

return toolOutput(
toolName,
output,
undefined,
"\n\n---\n\n" +
"The JSON above is additional context information about the topic that is being discussed. \n"+
"Answer the question using only the relevant entries in the context. " +
"Do not make a judgement on the quality of the results. " +
"You must use only the information provided in the context. If it doesn't answer the question, you say that you don't know." +
"You must cite your sources by providing the urls and similarity for every entry you used from the context." +
"You must answer in the same language as the original question, but you may use sources in different languages."

)
}
}
]

return {
name: toolName,
explanation: explanation,
methods: methods
}
}



function codeBlock(content: string, type?: string): string {
return '```' + (type ? type : '') + '\n' + content + '\n```'
}

export function renderToolInstructions(tool: ToolSpec): string {
const basePrompt = `You are an AI assistant called "ToolBot".
ToolBot is an intelligent chatbot that is designed to use the tool it is provided.
ToolBot understands the given tool descriptions and APIs.
Toolbot must choose a method and its arguments from the given tool.
ToolBot provides the chosen method and its step-by-step reasons formatted as JSON like this:
` + codeBlock(
`{
// Think step by step and provide a reasoning for what you think would be the best method to use.
reason: "",
// Your selected method
method: "",
// Your selected method's arguments
arguments: ["", ""]
}`, 'json') + '\n\n ToolBot always starts its output with `{\n"reason":"` and ends it with `]\n}`. \n\n'

const instructions = tool.methods?.map((method: MethodSpec) => {
const header = `## ${method.name} ${method.arguments.map(it => '<'+it.name+'>').join(' ')}\n`
const explanation = method.explanation + '\n' + 'Arguments: \n' + method.arguments.map(it => '* ' + it.name + ': ' + it.doc).join('\n') + '\n\n'
const examples = 'Examples: \n' + method.examples.map(it => `* \`${method.name} ${it.input}\` => \n${codeBlock(it.output)}\n\n`).join('\n') + '\n\n'

return `${header}${explanation}${method.examples ? examples : ''}`
}).join('\n\n')

return basePrompt + instructions
}

export function renderToolSelectionInstructions(tools: ToolSpec[]): string {
const basePrompt = `
You are "ToolSelector". The following is a list of tools and a description of their utility that are available. You will receive a query from a user. The query is part of a larger conversation that you don't have direct access to. Your responsibility is to select one of the tools to use to resolve the user's query.

You output everything as JSON formatted like the this:
` + codeBlock(`
{
// Think step by step and provide a reasoning for what you think would be the best tool to use.
reason: "",
// Your actual tool selection.
toolName: ""
}
`, 'json') + `
ToolSelector always starts its output with \`{\n"reason":"\` and ends it with \`"\n}\`. \n\n

---

`
const toolPrompt = tools.map(tool => `# ${tool.name}\n${tool.explanation}\n`).join('\n\n')

return basePrompt + toolPrompt
}

export const tools: ToolSpec[] = [
{
name: 'more-context-tool',
explanation: 'Select this tool when you need clarification. A tool that will make the user provide you with more context in the current conversation.',
},
{
name: 'noop',
explanation: 'A tool that signals that there is no need to use a tool.',
},
{
name: 'no-applicable-tool',
explanation: 'A tool that signals that you don\'t have a good tool to resolve the query.',
},
calculatorTool(),
encyclopediaTool('http://localhost:8080', '5', '64291804-d1d9-4e40-9f4f-9d145bc30882')
]
30 changes: 30 additions & 0 deletions src/lib/tools/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {Message} from "$lib/backend/types";

export interface ExampleSpec {
input: string,
output: string
}

export interface ArgumentSpec {
name: string,
doc: string
}

export interface ToolCall {
method: string,
arguments: string[]
}

export interface MethodSpec {
name: string,
arguments: ArgumentSpec[],
explanation: string,
examples: ExampleSpec[],
exec?: (toolSpec: ToolCall) => Promise<Message>
}

export interface ToolSpec {
name: string,
explanation: string,
methods?: MethodSpec[]
}
94 changes: 81 additions & 13 deletions src/routes/[conversationId]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import type { Message } from '$lib/backend/types';

import { prompt, confirm } from "$lib/components/dialogs";
import {renderToolInstructions, renderToolSelectionInstructions, tools} from "$lib/tools";
import type {MethodSpec, ToolCall, ToolSpec} from "$lib/tools/types";


let inputText = '';
Expand All @@ -41,6 +43,72 @@

$: conversationTitle = $currentConversation?.title || 'New conversation';

async function chooseTool(message){
const response = await $currentBackend.sendMessage([
{
role: 'system',
content: renderToolSelectionInstructions(tools) + "\n\n --- \n\n The User's query is: \n```" + message.content + "```"
}
])
let match = null;
try {
match = JSON.parse(response.content);
}catch (e) {
console.warn("Could not parse response from tool selector.", response);
}
if(match){
const selectedTool = match.toolName;
console.info("Selected Tool:", selectedTool)

const tool: ToolSpec = tools.find(tool => tool.name === selectedTool);
if(tool){
const toolFn = async () => {
const toolResponse = await $currentBackend.sendMessage([
{
role: 'system',
content: renderToolInstructions(tool)+ "\n\n --- \n\n The User's query is: ```" + message.content + "```"
}
])

try {
const toolCall: ToolCall = JSON.parse(toolResponse.content)
console.info("Tool Call:", toolCall.method, toolCall.arguments)

const method = tool.methods?.find(it => it.name === toolCall.method)
if(method){
return await method.exec(toolCall)
}else{
console.warn("Selected method not in list of methods.", toolCall);
}
}catch (e) {
console.warn("Could not parse response from tool.", toolResponse);
}
}
toolFn.toolName = selectedTool;
return toolFn;
}else{
console.warn("Selected tool not in list of tools.", response);
}
}
}

function getHistory(){
let history;
if (forkMessageId !== $currentConversation?.lastMessageId) {
const forkMessageIdx = $currentMessageThread.messages.findIndex(
(it) => it.self === forkMessageId
);
history = $currentMessageThread.messages
.slice(0, forkMessageIdx + 1)
.map((msg) => $currentConversation?.messages[msg.self].message);
} else {
history = $currentMessageThread.messages.map(
(msg) => $currentConversation?.messages[msg.self].message
);
}
return history;
}

async function sendMessageToChat() {
waiting = true;
let message: Message;
Expand All @@ -50,28 +118,28 @@
content: inputText
};


await addMessage(message, { backend: 'human', model: 'egg' }, forkMessageId);
forkMessageId = $currentConversation?.lastMessageId;
inputText = '';

if(!autoSend) {
waiting = false;
return;
}
}


let history;
if (forkMessageId !== $currentConversation?.lastMessageId) {
const forkMessageIdx = $currentMessageThread.messages.findIndex(
(it) => it.self === forkMessageId
);
history = $currentMessageThread.messages
.slice(0, forkMessageIdx + 1)
.map((msg) => $currentConversation?.messages[msg.self].message);
} else {
history = $currentMessageThread.messages.map(
(msg) => $currentConversation?.messages[msg.self].message
);
let history = getHistory();
if(history.length > 0 && history[history.length - 1].role === 'user'){
const tool = await chooseTool(history[history.length - 1]);
if(tool){
const toolOutputMessage = await tool();
if(toolOutputMessage){
await addMessage(toolOutputMessage, { backend: tool.toolName, model: 'tool' }, forkMessageId);
}
}
forkMessageId = $currentConversation?.lastMessageId;
history = getHistory();
}

const source = { backend: $currentBackend.name, model: $currentBackend.model };
Expand Down