Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: interactive shell example #150

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,6 @@ dist

last-run-id.txt
context-*.json

# Generated by the example apps
example/assets/nodejs-app
89 changes: 89 additions & 0 deletions example/src/ai_shell.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'dotenv/config'

import { createShellTools } from '@fabrice-ai/tools/shell'
import { agent } from 'fabrice-ai/agent'
import { tool } from 'fabrice-ai/tool'
import { workflow } from 'fabrice-ai/workflow'
import * as path from 'path'
import { z } from 'zod'

import { askUser } from './tools/askUser.js'

// @ts-ignore
export const workingDir = path.resolve(import.meta.dirname, '../assets/')
const shellTools = createShellTools({
mountPointDir: 'mnt',
workingDir,
dockerOptions: {},
escapeCommand: true,
})

export const print = tool({
description: 'Print information to the user tool',
execute: async (parameters) => {
console.log('🌟 ' + parameters.message)
return parameters.message
},
parameters: z.object({
message: z.string().describe('The message to be printed to the user.'),
}),
})

const greetingMaster = agent({
description: `
You are responsible for greeting the user.
Use the "print" tool to display the art and messages to the user`,
tools: {
print,
},
})

const shellOperator = agent({
description: `
You are skilled Linux master, knowing all things shell.
You role play being a linux shell where user enters what he/she wants to do in plain english.
You work in a sequence:
- "askUser" for a command,
- explains what you are about to do using "print" tool,
- executes the command using "shellTool" and prints the results using "print" tool.
You work on Alpine linux.
`,
tools: {
askUser,
shellExec: shellTools.shellExec,
print,
},
})

export const aiShellWorkflow = workflow({
team: { greetingMaster, shellOperator },
description: `
You are a interactive shell application that works on plain english.
User enters the next thing they want to do in simple english and you plan how to achieve it using
all sort of Alpine Linux shell commands.

For example user says: "List all folders" - and the agent translates it into command "ls -laht".
You try to print to the user what are you about to do - like: "To list all folders I will execute the 'ps' command ..."
then you executes this command, displays result to the user and ask for the next instruction.

Remember to greet the user first printing welcome message using some cool Emojis.

Welcome message: "Welcome to Interactive English Shell.
Tell me what you want to do and I'll translate it into shell commands"

`,
knowledge: `
- First print to the user what commands are you about to execute.
- You are on Alpine linux and have "shellExec" tool to run any shell command you want
- Your working directory is "/mnt/"
- Stop the workflow and stop asking user for next commands when instructed to do so - like they want to quit
- Do not use Markdown, use emojis instead for example 📁 when listing a folder etc.
- The Docker container persist between the calls so you can change directories etc.
- You can install required packages like nodejs, npm using "apk add --update <packagename>"
- You should create files and directories using standard linux tools - for example "echo".
`,
output: `
There's no single output. It's just an interactive app. You print the partial results printing it to console
`,
snapshot: () => {},
})
65 changes: 65 additions & 0 deletions example/src/ai_shell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'dotenv/config'

import { suite, test } from '@fabrice-ai/bdd/suite'
import { testwork } from '@fabrice-ai/bdd/testwork'
import { tool } from 'fabrice-ai/tool'
import { z } from 'zod'

import { aiShellWorkflow, print } from './ai_shell.config.js'

let idx = 0
export const askUserMock = tool({
description: 'Tool for asking user a question',
parameters: z.object({
query: z.string().describe('The question to ask the user'),
}),
execute: async ({ query }, { provider }): Promise<string> => {
const responses = ['List all files in the directory', 'I am done. Quit please.']
const response = responses[idx]
console.log(`😳 Mocked response: ${response}\n`)
if (idx < responses.length) idx += 1 // stay on the last response
return Promise.resolve(response)
},
})

const shellExecMock = tool({
description: 'Executes a shell command inside a Docker container. Storage path is configurable.',
parameters: z.object({
command: z.string().describe('The shell command to run inside the container.'),
}),
execute: async ({ command }) => {
console.log(`😳 Mocked shell command call: ${command}\n`)
return 'Executed!'
},
})

aiShellWorkflow.team['shellOperator'].tools = {
askUser: askUserMock,
shellExec: shellExecMock,
print,
}

const testResults = await testwork(
aiShellWorkflow,
suite({
description: 'Black box testing suite',
team: {
shellOperator: [
test('1_ask_for_command', 'There should be a tool call to "askUser" function'),
test(
'2_execute_command',
'There should be a tool call to "shellExec" function or when user answers with "i want to quit" it should end the workflow'
),
],
},
workflow: [test('0_greeting', 'Should display greeting message first')],
})
)

if (!testResults.passed) {
console.log('🚨 Test suite failed')
process.exit(-1)
} else {
console.log('✅ Test suite passed')
process.exit(0)
}
10 changes: 10 additions & 0 deletions example/src/ai_shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'dotenv/config'

import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'

import { aiShellWorkflow } from './ai_shell.config.js'

const result = await teamwork(aiShellWorkflow)

console.log(solution(result))
57 changes: 57 additions & 0 deletions example/src/python_random_jokes.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createCodeInterpreter } from '@fabrice-ai/tools/interpreter'
import { agent } from 'fabrice-ai/agent'
import { workflow } from 'fabrice-ai/workflow'
import path from 'path'

const joker = agent({
description: `
You are skilled at writing funny jokes.
User your skills to write the jokes.
`,
})

// @ts-ignore
export const workingDir = path.resolve(import.meta.dirname, '../assets/')
const coder = agent({
description: `
You are skilled at writing Python code.
`,
})

const runner = agent({
description: `
You are skilled at running Python code.
Can use the "codeInterpreter" tool to interpret the generated Python code.
`,
tools: createCodeInterpreter({
mountPointDir: 'mnt',
workingDir,
dockerOptions: {},
}),
})

export const randomJokesScriptWorkflow = workflow({
team: {
joker,
coder,
runner,
},
description: `
Our goal is to create and run a Python 3 script
that will display single random joke.

The jokes should prepared beforehand and included in Python source code.
Generate the code displaing to "stdout" a joke - including 2 random jokes as an array in the code.
Run the script using the "codeInterpreter" tool.
Display the output line it returned (a joke).`,
knowledge: `
Focus:
- Jokes should be funny, yet based on real facts
- The script must be executed by code interprteter
- The "codeInterpreter" tool should be executed exactly once
- "codeInterpreter" can install libraries but pass only non-built in libraries otherwise it will gets you an error
`,
output: `
The final output of the interpreting Python Script - a randomly selected joke.
`,
})
33 changes: 33 additions & 0 deletions example/src/python_random_jokes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dotenv/config'

import { suite, test } from '@fabrice-ai/bdd/suite'
import { testwork } from '@fabrice-ai/bdd/testwork'

import { randomJokesScriptWorkflow } from './python_random_jokes.config.js'

const testResults = await testwork(
randomJokesScriptWorkflow,
suite({
description: 'Black box testing suite',
team: {
coder: [test('2_jokes', 'Two jokes should be generated and passed within the Python code')],
runner: [
test(
'1_interpreter',
'The "codeInterpreter" tool should be called once to run the Python script'
),
],
},
workflow: [
test('2_final_result', `As a final result there should be one joke returned as a string`),
],
})
)

if (!testResults.passed) {
console.log('🚨 Test suite failed')
process.exit(-1)
} else {
console.log('✅ Test suite passed')
process.exit(0)
}
8 changes: 8 additions & 0 deletions example/src/python_random_jokes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'
import { randomJokesScriptWorkflow } from './python_random_jokes.config'


const result = await teamwork(randomJokesScriptWorkflow)
console.log(solution(result))
60 changes: 60 additions & 0 deletions example/src/shell_node_project.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'dotenv/config'

import { createShellTools } from '@fabrice-ai/tools/shell'
import { agent } from 'fabrice-ai/agent'
import { logger } from 'fabrice-ai/telemetry'
import { workflow } from 'fabrice-ai/workflow'
import * as path from 'path'

// @ts-ignore
export const workingDir = path.resolve(import.meta.dirname, '../assets/')
const shellTools = createShellTools({
mountPointDir: 'mnt',
workingDir,
dockerOptions: {},
escapeCommand: true,
})

const devops = agent({
description: `
You are skilled at operating all sort of Shell commands
You work on Alpine linux mostly.
`,
tools: {
shellExec: shellTools.shellExec,
},
})

const developer = agent({
description: `
You are skilled at writing NodeJS code
`,
tools: shellTools,
})

export const createHelloworldNodeProject = workflow({
team: { devops, developer },
description: `
Create a new NodeJS project displaing "Hello World" to console.
You must plan all necessary steps - including creating a new folder "nodejs-app", a node.js project inside and and a JS script doing the job.
You need to install all required dependencies to run it.
The script should display "Hello world" + random number.

`,
knowledge: `
- You are on Alpine linux and have "shellExec" tool to run any shell command you want
- Check if the directories or folders you are about to create doesn't exist and if so remove them.
- Save the script into "/mnt/nodejs-app/index.js" file before running it
- Create folder "/mnt/nodejs-app" for the project in the current working directory.
- Use the created follder for all subsequent operations
- Create and run app inside "/mnt/nodejs-app" folder only
- The Docker container persist between the calls so you can change directories etc.
- You can install required packages like nodejs, npm using "apk add --update <packagename>"
- You should create files and directories using standard linux tools - for example "echo"
- Operate only within a specially created folder called "nodejs-app". Don't modify any files oudside of it.
`,
output: `
Exact text generated to stdout - from index.js script created and executed using the "shellExec" commands.
`,
snapshot: logger,
})
53 changes: 53 additions & 0 deletions example/src/shell_node_project.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'dotenv/config'

import { suite, test } from '@fabrice-ai/bdd/suite'
import { testwork } from '@fabrice-ai/bdd/testwork'
import fs from 'fs'
import path from 'path'

import { createHelloworldNodeProject, workingDir } from './shell_node_project.config.js'

const testResults = await testwork(
createHelloworldNodeProject,
suite({
description: 'Black box testing suite',
team: {},
workflow: [
test('0_folders', 'App should work inside the nodejs-app directory'),
test(
'1_required_packages',
`Required packages should be installed using "apk" command - specifically nodejs, npm`
),

test('2_output', `Hello world + <random number> should be displayed as a final result`),
test(
'3_script',
`The generated file sould include "console.log" call in the ${path.join(workingDir, 'nodejs-app', 'index.js')} file`,
async (workflow, state) => {
const outputPath = path.join(workingDir, 'nodejs-app', 'index.js')
if (!fs.existsSync(outputPath)) {
return {
passed: false,
reasoning: `Output file ${outputPath} does not exist`,
id: '3_script',
}
}
const htmlContent = fs.readFileSync(outputPath, 'utf-8')
return {
reasoning: 'Output file includes the console.log call',
passed: htmlContent.includes('console.log'),
id: '3_script',
}
}
),
],
})
)

if (!testResults.passed) {
console.log('🚨 Test suite failed')
process.exit(-1)
} else {
console.log('✅ Test suite passed')
process.exit(0)
}
Loading