mirror of
https://github.com/humanlayer/humanlayer.git
synced 2025-08-20 19:01:22 +03:00
wip on humanlayer-ts example
This commit is contained in:
56
examples/ts_openai_client/index.js
Normal file
56
examples/ts_openai_client/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
var __awaiter =
|
||||
(this && this.__awaiter) ||
|
||||
function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) {
|
||||
return value instanceof P
|
||||
? value
|
||||
: new P(function (resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
}
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function rejected(value) {
|
||||
try {
|
||||
step(generator["throw"](value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
function step(result) {
|
||||
result.done
|
||||
? resolve(result.value)
|
||||
: adopt(result.value).then(fulfilled, rejected);
|
||||
}
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importDefault =
|
||||
(this && this.__importDefault) ||
|
||||
function (mod) {
|
||||
return mod && mod.__esModule ? mod : { default: mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const openai_1 = __importDefault(require("openai"));
|
||||
const openAIHello = () =>
|
||||
__awaiter(void 0, void 0, void 0, function* () {
|
||||
const openai = new openai_1.default({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
const response = yield openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "Hello, world!" }],
|
||||
});
|
||||
});
|
||||
const main = () =>
|
||||
__awaiter(void 0, void 0, void 0, function* () {
|
||||
yield openAIHello();
|
||||
});
|
||||
main().then(console.log).catch(console.error);
|
||||
@@ -1,18 +1,98 @@
|
||||
|
||||
|
||||
import { HumanLayer } from "humanlayer";
|
||||
import { FunctionCallSpec } from "humanlayer";
|
||||
import OpenAI from "openai";
|
||||
import { ChatCompletionTool } from "openai/src/resources/index.js";
|
||||
|
||||
const PROMPT = "multiply 2 and 5, then add 32 to the result";
|
||||
|
||||
const openAIHello = async () => {
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
const multiply = ({ a, b }: { a: number; b: number }) => a * b;
|
||||
const add = ({ a, b }: { a: number; b: number }) => a + b;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-3.5-turbo",
|
||||
messages: [{ role: "user", content: "Hello, world!" }],
|
||||
});
|
||||
const tools_map = {
|
||||
multiply: multiply,
|
||||
add: add,
|
||||
};
|
||||
|
||||
openAIHello().catch(console.error);
|
||||
const openai_tools: ChatCompletionTool[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "multiply",
|
||||
description: "multiply two numbers",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
a: { type: "number" },
|
||||
b: { type: "number" },
|
||||
},
|
||||
required: ["a", "b"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "add",
|
||||
description: "add two numbers",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
a: { type: "number" },
|
||||
b: { type: "number" },
|
||||
},
|
||||
required: ["a", "b"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const openAIHello = async (
|
||||
prompt: string,
|
||||
tools_map: { [key: string]: (args: any) => any },
|
||||
openai_tools: ChatCompletionTool[],
|
||||
) => {
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
const messages: any[] = [{ role: "user", content: prompt }];
|
||||
|
||||
let response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: messages,
|
||||
tools: openai_tools,
|
||||
tool_choice: "auto",
|
||||
});
|
||||
|
||||
while (response.choices[0].message.tool_calls) {
|
||||
messages.push(response.choices[0].message);
|
||||
const tool_calls = response.choices[0].message.tool_calls;
|
||||
for (const tool_call of tool_calls) {
|
||||
const tool_name = tool_call.function.name;
|
||||
const tool_args = JSON.parse(tool_call.function.arguments);
|
||||
console.log(
|
||||
`calling tools ${tool_name}(${tool_call.function.arguments})`,
|
||||
);
|
||||
const tool_result = tools_map[tool_name](tool_args);
|
||||
messages.push({
|
||||
role: "tool",
|
||||
name: tool_name,
|
||||
content: JSON.stringify(tool_result),
|
||||
tool_call_id: tool_call.id,
|
||||
});
|
||||
}
|
||||
|
||||
response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: messages,
|
||||
});
|
||||
}
|
||||
|
||||
return response.choices[0].message.content;
|
||||
};
|
||||
|
||||
const main = async (): Promise<any> => {
|
||||
const resp = await openAIHello(PROMPT, tools_map, openai_tools);
|
||||
return resp;
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
|
||||
1229
examples/ts_openai_client/package-lock.json
generated
Normal file
1229
examples/ts_openai_client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,10 @@
|
||||
"version": "0.1.0",
|
||||
"description": "example of using humanlayer with openai in typescript",
|
||||
"main": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "node --env-file=.env -r ts-node/register index.ts"
|
||||
"dev": "tsx --env-file=.env index.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# HumanLayer example using OpenAI client in Typescript
|
||||
|
||||
Set up env
|
||||
|
||||
```
|
||||
cp dotenv.example .env
|
||||
# configure API token(s)
|
||||
```
|
||||
|
||||
## Running with NPM
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## If you prefer docker
|
||||
|
||||
```
|
||||
docker compose run examples
|
||||
```
|
||||
|
||||
12
examples/ts_openai_client/tsconfig.json
Normal file
12
examples/ts_openai_client/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc",
|
||||
"root": true,
|
||||
"extends": [
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["prettier"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
|
||||
2
humanlayer-ts/.gitignore
vendored
2
humanlayer-ts/.gitignore
vendored
@@ -33,4 +33,4 @@ yarn-error.log*
|
||||
.turbo
|
||||
|
||||
.contentlayer
|
||||
.env
|
||||
.env
|
||||
|
||||
@@ -9,4 +9,4 @@ dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
.contentlayer
|
||||
|
||||
377
humanlayer-ts/src/approval.ts
Normal file
377
humanlayer-ts/src/approval.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import crypto from 'crypto'
|
||||
import { AgentBackend, HumanLayerException } from './protocol'
|
||||
import {
|
||||
ContactChannel,
|
||||
FunctionCall,
|
||||
FunctionCallSpec,
|
||||
HumanContact,
|
||||
HumanContactSpec,
|
||||
} from './models'
|
||||
import { CloudHumanLayerBackend, HumanLayerCloudConnection } from './cloud'
|
||||
import { logger } from './logger'
|
||||
|
||||
export enum ApprovalMethod {
|
||||
CLI = 'cli',
|
||||
BACKEND = 'backend',
|
||||
}
|
||||
|
||||
/**
|
||||
* sure this'll work for now
|
||||
*/
|
||||
export const default_genid = (prefix: string) => {
|
||||
return `${prefix}-${crypto.randomUUID().slice(0, 8)}`
|
||||
}
|
||||
|
||||
export class HumanLayer {
|
||||
approval_method: ApprovalMethod
|
||||
backend?: AgentBackend
|
||||
run_id: string
|
||||
agent_name: string
|
||||
genid: (prefix: string) => string
|
||||
sleep: (ms: number) => Promise<void>
|
||||
contact_channel?: ContactChannel
|
||||
|
||||
constructor({
|
||||
run_id,
|
||||
approval_method,
|
||||
backend,
|
||||
agent_name,
|
||||
genid,
|
||||
sleep,
|
||||
contact_channel,
|
||||
api_key,
|
||||
api_base_url,
|
||||
}: {
|
||||
run_id?: string
|
||||
approval_method?: ApprovalMethod
|
||||
backend?: AgentBackend
|
||||
agent_name?: string
|
||||
genid?: (prefix: string) => string
|
||||
sleep?: (ms: number) => Promise<void>
|
||||
contact_channel?: ContactChannel
|
||||
api_key?: string
|
||||
api_base_url?: string
|
||||
}) {
|
||||
this.genid = genid || default_genid
|
||||
this.sleep = sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)))
|
||||
this.contact_channel = contact_channel
|
||||
|
||||
if (!approval_method && process.env.HUMANLAYER_APPROVAL_METHOD) {
|
||||
const method = process.env.HUMANLAYER_APPROVAL_METHOD as keyof typeof ApprovalMethod
|
||||
if (method in ApprovalMethod) {
|
||||
this.approval_method = ApprovalMethod[method]
|
||||
} else {
|
||||
throw new Error(`Invalid HUMANLAYER_APPROVAL_METHOD: ${process.env.HUMANLAYER_APPROVAL_METHOD}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!approval_method) {
|
||||
if (backend || process.env.HUMANLAYER_API_KEY) {
|
||||
this.approval_method = ApprovalMethod.BACKEND
|
||||
this.backend =
|
||||
backend ||
|
||||
new CloudHumanLayerBackend(
|
||||
new HumanLayerCloudConnection(
|
||||
api_key || process.env.HUMANLAYER_API_KEY,
|
||||
api_base_url || process.env.HUMANLAYER_API_BASE_URL,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
logger.info('No HUMANLAYER_API_KEY found, defaulting to CLI approval')
|
||||
this.approval_method = ApprovalMethod.CLI
|
||||
}
|
||||
} else {
|
||||
this.approval_method = approval_method
|
||||
}
|
||||
|
||||
this.agent_name = agent_name || 'agent'
|
||||
this.genid = genid || default_genid
|
||||
this.run_id = run_id || this.genid(this.agent_name)
|
||||
|
||||
if (this.approval_method === ApprovalMethod.BACKEND && !backend) {
|
||||
throw new HumanLayerException('backend is required for non-cli approvals')
|
||||
}
|
||||
this.backend = backend
|
||||
}
|
||||
|
||||
static cloud({
|
||||
connection,
|
||||
api_key,
|
||||
api_base_url,
|
||||
}: {
|
||||
connection: HumanLayerCloudConnection | null
|
||||
api_key?: string
|
||||
api_base_url?: string
|
||||
}): HumanLayer {
|
||||
if (!connection) {
|
||||
connection = new HumanLayerCloudConnection(api_key, api_base_url)
|
||||
}
|
||||
return new HumanLayer({
|
||||
approval_method: ApprovalMethod.BACKEND,
|
||||
backend: new CloudHumanLayerBackend(connection),
|
||||
})
|
||||
}
|
||||
|
||||
static cli(): HumanLayer {
|
||||
return new HumanLayer({
|
||||
approval_method: ApprovalMethod.CLI,
|
||||
})
|
||||
}
|
||||
|
||||
require_approval<T_Fn extends Function>(contact_channel?: ContactChannel): (fn: T_Fn) => T_Fn {
|
||||
return (fn: T_Fn) => {
|
||||
if (this.approval_method === ApprovalMethod.CLI) {
|
||||
return this._approve_cli(fn)
|
||||
}
|
||||
|
||||
return this._approve_with_backend(fn, contact_channel)
|
||||
}
|
||||
}
|
||||
|
||||
_approve_cli<T_Fn extends Function>(fn: T_Fn): T_Fn {
|
||||
// todo fix the types here
|
||||
const f: any = (...args: any[]) => {
|
||||
console.log(`Agent ${this.run_id} wants to call
|
||||
|
||||
${fn.name}(${JSON.stringify(args, null, 2)})
|
||||
|
||||
${args.length ? ' with args: ' + JSON.stringify(args, null, 2) : ''}`)
|
||||
const feedback = prompt('Hit ENTER to proceed, or provide feedback to the agent to deny: \n\n')
|
||||
if (feedback !== null && feedback !== '') {
|
||||
return new Error(`User denied ${fn.name} with feedback: ${feedback}`)
|
||||
}
|
||||
try {
|
||||
return fn(...args)
|
||||
} catch (e) {
|
||||
return `Error running ${fn.name}: ${e}`
|
||||
}
|
||||
}
|
||||
f.name = fn.name
|
||||
return f
|
||||
}
|
||||
|
||||
_approve_with_backend<T_Fn extends Function>(fn: T_Fn, contact_channel?: ContactChannel): T_Fn {
|
||||
// todo fix the types here
|
||||
const f: any = async (...args: any[]) => {
|
||||
const backend = this.backend!
|
||||
const call_id = this.genid('call')
|
||||
await backend.functions().add({
|
||||
run_id: this.run_id,
|
||||
call_id,
|
||||
spec: {
|
||||
fn: fn.name,
|
||||
kwargs: args,
|
||||
channel: contact_channel,
|
||||
},
|
||||
})
|
||||
while (true) {
|
||||
await this.sleep(3000)
|
||||
const function_call = await backend.functions().get(call_id)
|
||||
if (function_call.status?.approved) {
|
||||
return fn(...args)
|
||||
} else {
|
||||
return function_call.status?.comment
|
||||
}
|
||||
}
|
||||
}
|
||||
f.name = fn.name
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
class HumanLayer(BaseModel):
|
||||
"""HumanLayer"""
|
||||
|
||||
def require_approval(
|
||||
self,
|
||||
contact_channel: ContactChannel | None = None,
|
||||
) -> HumanLayerWrapper:
|
||||
def decorator(fn): # type: ignore
|
||||
if self.approval_method is ApprovalMethod.CLI:
|
||||
return self._approve_cli(fn)
|
||||
|
||||
return self._approve_with_backend(fn, contact_channel)
|
||||
|
||||
return HumanLayerWrapper(decorator)
|
||||
|
||||
def _approve_cli(self, fn: Callable[[T], R]) -> Callable[[T], R | str]:
|
||||
"""
|
||||
NOTE we convert a callable[[T], R] to a Callable [[T], R | str]
|
||||
|
||||
this is safe to do for most LLM use cases. It will blow up
|
||||
a normal function.
|
||||
|
||||
If we can guarantee the function calling framework
|
||||
is properly handling exceptions, then we can
|
||||
just raise and let the framework handle the stringification
|
||||
of what went wrong.
|
||||
|
||||
Because some frameworks dont handle exceptions well, were stuck with the hack for now
|
||||
"""
|
||||
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs) -> R | str: # type: ignore
|
||||
print(
|
||||
f"""Agent {self.run_id} wants to call
|
||||
|
||||
{fn.__name__}({json.dumps(kwargs, indent=2)})
|
||||
|
||||
{"" if not args else " with args: " + str(args)}"""
|
||||
)
|
||||
feedback = input("Hit ENTER to proceed, or provide feedback to the agent to deny: \n\n")
|
||||
if feedback not in {
|
||||
None,
|
||||
"",
|
||||
}:
|
||||
return str(UserDeniedError(f"User denied {fn.__name__} with feedback: {feedback}"))
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except Exception as e:
|
||||
return f"Error running {fn.__name__}: {e}"
|
||||
|
||||
return wrapper
|
||||
|
||||
def _approve_with_backend(
|
||||
self,
|
||||
fn: Callable[[T], R],
|
||||
contact_channel: ContactChannel | None = None,
|
||||
) -> Callable[[T], R | str]:
|
||||
"""
|
||||
NOTE we convert a callable[[T], R] to a Callable [[T], R | str]
|
||||
|
||||
this is safe to do for most LLM use cases. It will blow up
|
||||
a normal function.
|
||||
|
||||
If we can guarantee the function calling framework
|
||||
is properly handling exceptions, then we can
|
||||
just raise and let the framework handle the stringification
|
||||
of what went wrong.
|
||||
|
||||
Because some frameworks dont handle exceptions well, were stuck with the hack for now
|
||||
"""
|
||||
contact_channel = contact_channel or self.contact_channel
|
||||
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs) -> R | str: # type: ignore
|
||||
assert self.backend is not None
|
||||
call_id = self.genid("call")
|
||||
try:
|
||||
call = FunctionCall(
|
||||
run_id=self.run_id, # type: ignore
|
||||
call_id=call_id,
|
||||
spec=FunctionCallSpec(
|
||||
fn=fn.__name__,
|
||||
kwargs=kwargs,
|
||||
channel=contact_channel,
|
||||
),
|
||||
)
|
||||
self.backend.functions().add(call)
|
||||
|
||||
# todo lets do a more async-y websocket soon
|
||||
while True:
|
||||
self.sleep(3)
|
||||
function_call: FunctionCall = self.backend.functions().get(call_id)
|
||||
if function_call.status is None or function_call.status.approved is None:
|
||||
continue
|
||||
|
||||
if function_call.status.approved:
|
||||
return fn(*args, **kwargs)
|
||||
else:
|
||||
if (
|
||||
function_call.spec.channel
|
||||
and function_call.spec.channel.slack
|
||||
and function_call.spec.channel.slack.context_about_channel_or_user
|
||||
):
|
||||
return f"User in {function_call.spec.channel.slack.context_about_channel_or_user} denied {fn.__name__} with message: {function_call.status.comment}"
|
||||
elif (
|
||||
contact_channel
|
||||
and contact_channel.slack
|
||||
and contact_channel.slack.context_about_channel_or_user
|
||||
):
|
||||
return f"User in {contact_channel.slack.context_about_channel_or_user} denied {fn.__name__} with message: {function_call.status.comment}"
|
||||
else:
|
||||
return f"User denied {fn.__name__} with message: {function_call.status.comment}"
|
||||
except Exception as e:
|
||||
logger.exception("Error requesting approval")
|
||||
# todo - raise vs. catch behavior - many tool clients handle+wrap errors
|
||||
# but not all of them :rolling_eyes:
|
||||
return f"Error running {fn.__name__}: {e}"
|
||||
|
||||
return wrapper
|
||||
|
||||
def human_as_tool(
|
||||
self,
|
||||
contact_channel: ContactChannel | None = None,
|
||||
) -> Callable[[str], str]:
|
||||
if self.approval_method is ApprovalMethod.CLI:
|
||||
return self._human_as_tool_cli()
|
||||
|
||||
return self._human_as_tool(contact_channel)
|
||||
|
||||
def _human_as_tool_cli(
|
||||
self,
|
||||
) -> Callable[[str], str]:
|
||||
def contact_human(
|
||||
question: str,
|
||||
) -> str:
|
||||
"""ask a human a question on the CLI"""
|
||||
print(
|
||||
f"""Agent {self.run_id} requests assistance:
|
||||
|
||||
{question}
|
||||
"""
|
||||
)
|
||||
feedback = input("Please enter a response: \n\n")
|
||||
return feedback
|
||||
|
||||
return contact_human
|
||||
|
||||
def _human_as_tool(
|
||||
self,
|
||||
contact_channel: ContactChannel | None = None,
|
||||
) -> Callable[[str], str]:
|
||||
contact_channel = contact_channel or self.contact_channel
|
||||
|
||||
def contact_human(
|
||||
message: str,
|
||||
) -> str:
|
||||
"""contact a human"""
|
||||
assert self.backend is not None
|
||||
call_id = self.genid("human_call")
|
||||
|
||||
contact = HumanContact(
|
||||
run_id=self.run_id, # type: ignore
|
||||
call_id=call_id,
|
||||
spec=HumanContactSpec(
|
||||
msg=message,
|
||||
channel=contact_channel,
|
||||
),
|
||||
)
|
||||
self.backend.contacts().add(contact)
|
||||
|
||||
# todo lets do a more async-y websocket soon
|
||||
while True:
|
||||
self.sleep(3)
|
||||
human_contact = self.backend.contacts().get(call_id)
|
||||
if human_contact.status is None:
|
||||
continue
|
||||
|
||||
if human_contact.status.response is not None:
|
||||
return human_contact.status.response
|
||||
|
||||
if contact_channel is None:
|
||||
return contact_human
|
||||
|
||||
if contact_channel.slack:
|
||||
contact_human.__doc__ = "Contact a human via slack and wait for a response"
|
||||
contact_human.__name__ = "contact_human_in_slack"
|
||||
if contact_channel.slack.context_about_channel_or_user:
|
||||
contact_human.__doc__ += f" in {contact_channel.slack.context_about_channel_or_user}"
|
||||
fn_ctx = contact_channel.slack.context_about_channel_or_user.replace(" ", "_")
|
||||
contact_human.__name__ = f"contact_human_in_slack_in_{fn_ctx}"
|
||||
|
||||
return contact_human
|
||||
|
||||
*/
|
||||
116
humanlayer-ts/src/cloud.ts
Normal file
116
humanlayer-ts/src/cloud.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { AgentBackend, AgentStore, HumanLayerException } from './protocol'
|
||||
import { FunctionCall, HumanContact } from './models'
|
||||
|
||||
class HumanLayerCloudConnection {
|
||||
api_key?: string
|
||||
api_base_url?: string
|
||||
|
||||
constructor(api_key?: string, api_base_url?: string) {
|
||||
this.api_key = api_key
|
||||
this.api_base_url = api_base_url
|
||||
|
||||
if (!this.api_key) {
|
||||
throw new Error('HUMANLAYER_API_KEY is required for cloud approvals')
|
||||
}
|
||||
this.api_base_url = this.api_base_url || 'https://api.humanlayer.dev/humanlayer/v1'
|
||||
// todo ping api to validate token
|
||||
}
|
||||
|
||||
async request({
|
||||
method,
|
||||
path,
|
||||
body,
|
||||
}: {
|
||||
method: string
|
||||
path: string
|
||||
body?: any
|
||||
}): Promise<Response> {
|
||||
const resp = await fetch(`${this.api_base_url}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`,
|
||||
},
|
||||
body,
|
||||
})
|
||||
|
||||
if (resp.status >= 400) {
|
||||
throw new HumanLayerException(`${method} ${path} ${resp.status}: ${await resp.text()}`)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
class CloudFunctionCallStore implements AgentStore<FunctionCall> {
|
||||
private connection: HumanLayerCloudConnection
|
||||
|
||||
constructor(connection: HumanLayerCloudConnection) {
|
||||
this.connection = connection
|
||||
}
|
||||
|
||||
async add(item: FunctionCall): Promise<void> {
|
||||
await this.connection.request({
|
||||
method: 'POST',
|
||||
path: '/function_calls',
|
||||
body: JSON.stringify(item),
|
||||
})
|
||||
}
|
||||
|
||||
async get(call_id: string): Promise<FunctionCall> {
|
||||
const resp = await this.connection.request({
|
||||
method: 'GET',
|
||||
path: `/function_calls/${call_id}`,
|
||||
})
|
||||
const data = await resp.json()
|
||||
return data as FunctionCall
|
||||
}
|
||||
}
|
||||
|
||||
class CloudHumanContactStore implements AgentStore<HumanContact> {
|
||||
private connection: HumanLayerCloudConnection
|
||||
|
||||
constructor(connection: HumanLayerCloudConnection) {
|
||||
this.connection = connection
|
||||
}
|
||||
|
||||
async add(item: HumanContact): Promise<void> {
|
||||
const resp = await this.connection.request({
|
||||
method: 'POST',
|
||||
path: '/contact_requests',
|
||||
body: JSON.stringify(item),
|
||||
})
|
||||
}
|
||||
|
||||
async get(call_id: string): Promise<HumanContact> {
|
||||
const resp = await this.connection.request({
|
||||
method: 'GET',
|
||||
path: `/contact_requests/${call_id}`,
|
||||
})
|
||||
const data = await resp.json()
|
||||
return data as HumanContact
|
||||
}
|
||||
}
|
||||
|
||||
class CloudHumanLayerBackend implements AgentBackend {
|
||||
private _function_calls: CloudFunctionCallStore
|
||||
private _human_contacts: CloudHumanContactStore
|
||||
|
||||
constructor(connection: HumanLayerCloudConnection) {
|
||||
this._function_calls = new CloudFunctionCallStore(connection)
|
||||
this._human_contacts = new CloudHumanContactStore(connection)
|
||||
}
|
||||
|
||||
functions(): CloudFunctionCallStore {
|
||||
return this._function_calls
|
||||
}
|
||||
|
||||
contacts(): CloudHumanContactStore {
|
||||
return this._human_contacts
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
HumanLayerCloudConnection,
|
||||
CloudFunctionCallStore,
|
||||
CloudHumanContactStore,
|
||||
CloudHumanLayerBackend,
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
console.log("sucessfully installed humanlayer-ts")
|
||||
export * from './models'
|
||||
export * from './approval'
|
||||
export * from './cloud'
|
||||
|
||||
12
humanlayer-ts/src/logger.ts
Normal file
12
humanlayer-ts/src/logger.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// TODO: maybe use a real lib
|
||||
export const logger = {
|
||||
debug: (...args: any[]) => {
|
||||
console.debug(...args)
|
||||
},
|
||||
info: (...args: any[]) => {
|
||||
console.info(...args)
|
||||
},
|
||||
error: (...args: any[]) => {
|
||||
console.error(...args)
|
||||
},
|
||||
}
|
||||
69
humanlayer-ts/src/models.ts
Normal file
69
humanlayer-ts/src/models.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
type FunctionCallStatus = {
|
||||
requested_at: Date
|
||||
responded_at?: Date
|
||||
approved?: boolean
|
||||
comment?: string
|
||||
}
|
||||
|
||||
type SlackContactChannel = {
|
||||
// the slack channel or user id to contact
|
||||
channel_or_user_id: string
|
||||
// the context about the channel or user to contact
|
||||
context_about_channel_or_user?: string
|
||||
// the bot token to use to contact the channel or user
|
||||
bot_token?: string
|
||||
}
|
||||
|
||||
type ContactChannel = {
|
||||
slack?: SlackContactChannel
|
||||
}
|
||||
|
||||
type FunctionCallSpec = {
|
||||
// the function name to call
|
||||
fn: string
|
||||
// the function arguments
|
||||
kwargs: Record<string, any>
|
||||
// the contact channel to use to contact the human
|
||||
channel?: ContactChannel
|
||||
}
|
||||
|
||||
type FunctionCall = {
|
||||
// the run id
|
||||
run_id: string
|
||||
call_id: string
|
||||
spec: FunctionCallSpec
|
||||
status?: FunctionCallStatus
|
||||
}
|
||||
|
||||
type HumanContactSpec = {
|
||||
// the message to send to the human
|
||||
msg: string
|
||||
// the contact channel to use to contact the human
|
||||
channel?: ContactChannel
|
||||
}
|
||||
|
||||
type HumanContactStatus = {
|
||||
// the response from the human
|
||||
response: string
|
||||
}
|
||||
|
||||
type HumanContact = {
|
||||
// the run id
|
||||
run_id: string
|
||||
// the call id
|
||||
call_id: string
|
||||
// the spec for the human contact
|
||||
spec: HumanContactSpec
|
||||
status?: HumanContactStatus
|
||||
}
|
||||
|
||||
export {
|
||||
FunctionCallStatus,
|
||||
SlackContactChannel,
|
||||
ContactChannel,
|
||||
FunctionCallSpec,
|
||||
FunctionCall,
|
||||
HumanContactSpec,
|
||||
HumanContactStatus,
|
||||
HumanContact,
|
||||
}
|
||||
23
humanlayer-ts/src/protocol.ts
Normal file
23
humanlayer-ts/src/protocol.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FunctionCall, FunctionCallStatus, HumanContact, HumanContactStatus } from './models'
|
||||
|
||||
export type AgentStore<T_Call> = {
|
||||
add: (item: T_Call) => Promise<void>
|
||||
get: (call_id: string) => Promise<T_Call>
|
||||
}
|
||||
|
||||
export type AdminStore<T_Call, T_Status> = {
|
||||
respond: (call_id: string, status: T_Status) => Promise<void>
|
||||
list: (call_id: string) => Promise<Iterable<T_Call>>
|
||||
}
|
||||
|
||||
export class HumanLayerException extends Error {}
|
||||
|
||||
export type AgentBackend = {
|
||||
functions(): AgentStore<FunctionCall>
|
||||
contacts(): AgentStore<HumanContact>
|
||||
}
|
||||
|
||||
export type AdminBackend = {
|
||||
functions(): AdminStore<FunctionCall, FunctionCallStatus>
|
||||
contacts(): AdminStore<HumanContact, HumanContactStatus>
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
|
||||
type FunctionCallStatus = {
|
||||
requested_at: Date
|
||||
responded_at: Date | null
|
||||
approved: boolean | null
|
||||
comment: string | null
|
||||
}
|
||||
|
||||
type SlackContactChannel = {
|
||||
// the slack channel or user id to contact
|
||||
channel_or_user_id: string
|
||||
// the context about the channel or user to contact
|
||||
context_about_channel_or_user: string | null
|
||||
// the bot token to use to contact the channel or user
|
||||
bot_token: string | null
|
||||
}
|
||||
|
||||
type ContactChannel = {
|
||||
slack: SlackContactChannel | null
|
||||
}
|
||||
|
||||
type FunctionCallSpec = {
|
||||
// the function name to call
|
||||
fn: string
|
||||
// the function arguments
|
||||
kwargs: Record<string, any>
|
||||
// the contact channel to use to contact the human
|
||||
channel: ContactChannel | null
|
||||
}
|
||||
|
||||
type FunctionCall = {
|
||||
// the run id
|
||||
run_id: string
|
||||
call_id: string
|
||||
spec: FunctionCallSpec
|
||||
status: FunctionCallStatus | null
|
||||
}
|
||||
|
||||
type HumanContactSpec = {
|
||||
// the message to send to the human
|
||||
msg: string
|
||||
// the contact channel to use to contact the human
|
||||
channel: ContactChannel | null
|
||||
}
|
||||
|
||||
type HumanContactStatus = {
|
||||
// the response from the human
|
||||
response: string
|
||||
}
|
||||
|
||||
type HumanContact = {
|
||||
// the run id
|
||||
run_id: string
|
||||
// the call id
|
||||
call_id: string
|
||||
// the spec for the human contact
|
||||
spec: HumanContactSpec
|
||||
status: HumanContactStatus | null
|
||||
}
|
||||
|
||||
export {
|
||||
FunctionCallStatus,
|
||||
SlackContactChannel,
|
||||
ContactChannel,
|
||||
FunctionCallSpec,
|
||||
FunctionCall,
|
||||
HumanContactSpec,
|
||||
HumanContactStatus,
|
||||
HumanContact,
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
@@ -25,7 +25,7 @@
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
@@ -71,12 +71,12 @@
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
@@ -98,6 +98,6 @@
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ max-complexity = 12
|
||||
line_length = 104
|
||||
|
||||
[tool.deptry]
|
||||
exclude = ["examples", "venv", ".venv"]
|
||||
exclude = ["examples", "venv", ".venv", "humanlayer-ts"]
|
||||
|
||||
|
||||
[tool.deptry.per_rule_ignores]
|
||||
|
||||
Reference in New Issue
Block a user