Skip to content

Commit

Permalink
Add support for agents and agent deployment to the Node-based Fixie C…
Browse files Browse the repository at this point in the history
…LI. (#255)

This PR allows us to deploy Fixie Agents from the node-based CLI. It
also adds additional commands for viewing and manipulating agents.

There are also a few cleanups and fixes.
  • Loading branch information
Matt Welsh authored Sep 5, 2023
1 parent a8c991d commit 34cb97c
Show file tree
Hide file tree
Showing 5 changed files with 733 additions and 15 deletions.
13 changes: 11 additions & 2 deletions packages/fixie/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@fixieai/fixie",
"version": "1.0.1",
"version": "1.0.3",
"license": "MIT",
"repository": "fixie-ai/ai-jsx",
"bugs": "https://github.com/fixie-ai/ai-jsx/issues",
"homepage": "https://fixie.ai",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"start": "node --no-warnings dist/main.js",
"build-start": "yarn run build && yarn run start",
"format": "prettier --write .",
"lint": "eslint ."
Expand All @@ -18,12 +18,21 @@
],
"bin": "./dist/main.js",
"dependencies": {
"@apollo/client": "^3.8.1",
"apollo-upload-client": "^17.0.0",
"commander": "^11.0.0",
"extract-files": "^13.0.0",
"graphql": "^16.8.0",
"js-yaml": "^4.1.0",
"open": "^9.1.0",
"ora": "^7.0.1",
"terminal-kit": "^3.0.0"
},
"devDependencies": {
"@tsconfig/node18": "^2.0.1",
"@types/apollo-upload-client": "^17.0.2",
"@types/extract-files": "^8.1.1",
"@types/js-yaml": "^4.0.5",
"@types/node": "^20.4.1",
"@types/terminal-kit": "^2.5.1",
"@typescript-eslint/eslint-plugin": "^5.60.0",
Expand Down
288 changes: 288 additions & 0 deletions packages/fixie/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { gql } from '@apollo/client/core';
import yaml from 'js-yaml';
import fs from 'fs';
import terminal from 'terminal-kit';
import { execSync } from 'child_process';
import ora from 'ora';
import os from 'os';
import path from 'path';

const { terminal: term } = terminal;

import { FixieClient } from './client.js';

/** Represents metadata about an agent managed by the Fixie service. */
export interface AgentMetadata {
agentId: string;
handle: string;
name?: string;
description?: string;
moreInfoUrl?: string;
created: Date;
modified: Date;
owner: string;
}

/** Represents the contents of an agent.yaml configuration file. */
export interface AgentConfig {
handle: string;
name?: string;
description?: string;
moreInfoUrl?: string;
}

/** Represents metadata about an agent revision. */
export interface AgentRevision {
id: string;
created: Date;
}

/**
* This class provides an interface to the Fixie Agent API.
*/
export class FixieAgent {
owner: string;
handle: string;

/** Use GetAgent or CreateAgent instead. */
private constructor(readonly client: FixieClient, readonly agentId: string, public metadata: AgentMetadata) {
const parts = agentId.split('/');
this.owner = parts[0];
this.handle = parts[1];
}

/** Return the URL for this agent's page on Fixie. */
public agentUrl(): string {
return `${this.client.url}/agents/${this.agentId}`;
}

/** Get the agent with the given agent ID. */
public static async GetAgent(client: FixieClient, agentId: string): Promise<FixieAgent> {
const metadata = await FixieAgent.getAgentById(client, agentId);
const agent = new FixieAgent(client, agentId, metadata);
return agent;
}

/** Return all agents visible to the user. */
public static async ListAgents(client: FixieClient): Promise<FixieAgent[]> {
const result = await client.gqlClient().query({
fetchPolicy: 'no-cache',
query: gql`
{
allAgents {
agentId
}
}
`,
});
return Promise.all(
result.data.allAgents.map(async (agent: any) => {
const retAgent = await this.GetAgent(client, agent.agentId);
return retAgent;
})
);
}

/** Return the metadata associated with the given agent. */
private static async getAgentById(client: FixieClient, agentId: string): Promise<AgentMetadata> {
const result = await client.gqlClient().query({
fetchPolicy: 'no-cache',
query: gql`
query GetAgentById($agentId: String!) {
agent: agentById(agentId: $agentId) {
agentId
handle
name
description
moreInfoUrl
created
modified
owner {
__typename
... on UserType {
username
}
... on OrganizationType {
handle
}
}
}
}
`,
variables: { agentId },
});

return {
agentId: result.data.agent.agentId,
handle: result.data.agent.handle,
name: result.data.agent.name,
description: result.data.agent.description,
moreInfoUrl: result.data.agent.moreInfoUrl,
created: new Date(result.data.agent.created),
modified: new Date(result.data.agent.modified),
owner: result.data.agent.owner.username || result.data.agent.owner.handle,
};
}

/** Create a new Agent. */
public static async CreateAgent(
client: FixieClient,
handle: string,
name?: string,
description?: string,
moreInfoUrl?: string
): Promise<FixieAgent> {
const result = await client.gqlClient().mutate({
mutation: gql`
mutation CreateAgent($handle: String!, $description: String, $moreInfoUrl: String) {
createAgent(agentData: { handle: $handle, description: $description, moreInfoUrl: $moreInfoUrl }) {
agent {
agentId
}
}
}
`,
variables: {
handle,
name,
description,
moreInfoUrl,
},
});
const agentId = result.data.createAgent.agent.agentId;
return FixieAgent.GetAgent(client, agentId);
}

/** Delete this agent. */
delete() {
return this.client.gqlClient().mutate({
mutation: gql`
mutation DeleteAgent($handle: String!) {
deleteAgent(agentData: { handle: $handle }) {
agent {
handle
}
}
}
`,
variables: { handle: this.handle },
});
}

/** Update this agent. */
async update(name?: string, description?: string, moreInfoUrl?: string) {
this.client.gqlClient().mutate({
mutation: gql`
mutation UpdateAgent($handle: String!, $name: String, $description: String, $moreInfoUrl: String) {
updateAgent(
agentData: { handle: $handle, name: $name, description: $description, moreInfoUrl: $moreInfoUrl }
) {
agent {
agentId
}
}
}
`,
variables: {
handle: this.handle,
name,
description,
moreInfoUrl,
},
});
this.metadata = await FixieAgent.getAgentById(this.client, this.agentId);
}

/** Load an agent configuration from the given directory. */
public static LoadConfig(agentPath: string): AgentConfig {
const config = yaml.load(fs.readFileSync(`${agentPath}/agent.yaml`, 'utf8')) as AgentConfig;
return config;
}

/** Package the code in the given directory and return the path to the tarball. */
private static getCodePackage(agentPath: string): string {
// Read the package.json file to get the package name and version.
const packageJson = JSON.parse(fs.readFileSync(`${agentPath}/package.json`, 'utf8'));

// Create a temporary directory and run `npm pack` inside.
const tempdir = fs.mkdtempSync(path.join(os.tmpdir(), `fixie-tmp-${packageJson.name}-${packageJson.version}-`));
const commandline = `npm pack ${agentPath} --silent >/dev/null`;
execSync(commandline, { cwd: tempdir });
return `${tempdir}/${packageJson.name}-${packageJson.version}.tgz`;
}

/** Create a new agent revision, which deploys the agent. */
private async createRevision(tarball: string): Promise<string> {
const uploadFile = fs.readFileSync(fs.realpathSync(tarball));

const result = await this.client.gqlClient().mutate({
mutation: gql`
mutation CreateAgentRevision($handle: String!, $codePackage: Upload!) {
createAgentRevision(
agentHandle: $handle
revision: { managedDeployment: { environment: NODEJS, codePackage: $codePackage } }
) {
revision {
id
}
}
}
`,
variables: {
handle: this.handle,
codePackage: new Blob([uploadFile], { type: 'application/gzip' }),
},
fetchPolicy: 'no-cache',
});

return result.data.createAgentRevision.revision.id;
}

/** Deploy an agent from the given directory. */
public static async DeployAgent(client: FixieClient, agentPath: string): Promise<string> {
const config = await FixieAgent.LoadConfig(agentPath);
const agentId = `${(await client.userInfo()).username}/${config.handle}`;
term('🦊 Deploying agent ').green(agentId)('...\n');

// Check if the package.json path exists in this directory.
if (!fs.existsSync(`${agentPath}/package.json`)) {
throw Error(`No package.json found in ${agentPath}. Only JS-based agents are supported.`);
}

let agent: FixieAgent;
try {
agent = await FixieAgent.GetAgent(client, agentId);
term('👽 Updating agent ').green(agentId)('...\n');
agent.update(config.name, config.description, config.moreInfoUrl);
} catch (e) {
// Try to create the agent instead.
term('🌲 Creating new agent ').green(agentId)('...\n');
agent = await FixieAgent.CreateAgent(client, config.handle, config.name, config.description, config.moreInfoUrl);
}
const tarball = FixieAgent.getCodePackage(agentPath);
const spinner = ora(' 🚀 Deploying...').start();
const revision = await agent.createRevision(tarball);
spinner.succeed(`Revision ${revision} was deployed to ${agent.agentUrl()}`);
return revision;
}

/** Get the current agent revision. */
public async getCurrentRevision(): Promise<AgentRevision> {
const result = await this.client.gqlClient().query({
fetchPolicy: 'no-cache',
query: gql`
query GetRevisionId($agentId: String!) {
agentById(agentId: $agentId) {
currentRevision {
id
created
}
}
}
`,
variables: { agentId: this.agentId },
});

return result.data.agentById.currentRevision as AgentRevision;
}
}
Loading

3 comments on commit 34cb97c

@vercel
Copy link

@vercel vercel bot commented on 34cb97c Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ai-jsx-docs – ./packages/docs

ai-jsx-docs.vercel.app
ai-jsx-docs-git-main-fixie-ai.vercel.app
ai-jsx-docs-fixie-ai.vercel.app
docs.ai-jsx.com

@vercel
Copy link

@vercel vercel bot commented on 34cb97c Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ai-jsx-tutorial-nextjs – ./packages/tutorial-nextjs

ai-jsx-tutorial-nextjs-git-main-fixie-ai.vercel.app
ai-jsx-tutorial-nextjs-fixie-ai.vercel.app
ai-jsx-tutorial-nextjs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 34cb97c Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ai-jsx-nextjs-demo – ./packages/nextjs-demo

ai-jsx-nextjs-demo-git-main-fixie-ai.vercel.app
ai-jsx-nextjs-demo.vercel.app
ai-jsx-nextjs-demo-fixie-ai.vercel.app

Please sign in to comment.