Skip to content

Commit

Permalink
Agents are rough
Browse files Browse the repository at this point in the history
  • Loading branch information
awhiteside1 committed Sep 27, 2024
1 parent 78d91a9 commit a7c6534
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 141 deletions.
137 changes: 0 additions & 137 deletions packages/agents/src/Agent.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { HelloAgent, Task } from "./Agent";
import { HelloAgent } from "../agents/HelloAgent";
import { Effect } from "effect";
import { Schema } from "@effect/schema";
import { AppLayer } from "./services";
import { AppLayer } from "../services";
import { Task } from "../task";

describe("Agent", () => {
it("should ", { timeout: 100000 }, async () => {
Expand All @@ -18,7 +19,6 @@ describe("Agent", () => {
return yield* agent.process(createTask);
});

// const ollamaLayer = LLMServiceLive();
const runnable = Effect.provide(program, AppLayer);

const output = await Effect.runPromise(runnable);
Expand Down
56 changes: 56 additions & 0 deletions packages/agents/src/Agent/Agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Schema } from "@effect/schema";
import { LLMService } from "../services";
import { Effect } from "effect";
import { extractJSON, getMdast } from "../md";
import { getSectionByHeading } from "../md/getSection";
import { isRight } from "effect/Either";
import { Task, TaskResult } from "../task";
import { Prompts } from "./prompts";
import { InjectSection } from "./injectSection";

export abstract class Agent {
abstract name: string;
abstract description: string;

constructor() {}

generatePrompt(task: Task) {
const about = InjectSection(
"About You",
[
`You are a ${this.name}, responsible for ${this.description}.`,
"You are part of a multi-agent system which helps humans solve complex problems.",
].join("\n"),
);

const details = [
about,
Prompts.responseConventions,
InjectSection("Instructions", task.instructions),
InjectSection("Acceptance Criteria", task.acceptanceCriteria),
InjectSection("Schema (JsonSchema)", task.format),
];

return details.join("\n\n");
}

process(task: Effect.Effect<Readonly<Task>, Error, never>) {
const generatePrompt = this.generatePrompt.bind(this);
return Effect.gen(function* () {
const llm = yield* LLMService;

const taskReal = yield* task;
const prompt = generatePrompt(taskReal);
const response = yield* llm.generateText(prompt, "mistral-small");

const mdast = yield* getMdast(response);
const data = Schema.decodeEither(taskReal.format)(
extractJSON(getSectionByHeading(mdast, Prompts.sectionKeys.data)),
);
if (isRight(data)) {
return new TaskResult(data.right, "");
}
return new TaskResult(null, "null");
});
}
}
19 changes: 19 additions & 0 deletions packages/agents/src/Agent/injectSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { JSONSchema, Schema } from "@effect/schema";
import { Match } from "effect";
import { isArray } from "radash";
import { isSchema } from "@effect/schema/Schema";

type SupportedFormats = string | string[] | Schema.Schema.AnyNoContext;

const formatString = Match.type<SupportedFormats>().pipe(
Match.when(Match.string, (_) => _),
Match.when(isArray, (_) => _.map((_) => `- ${_}`).join("\n")),
Match.when(isSchema, (_) => JSON.stringify(JSONSchema.make(_))),
Match.when(Match.record, (_) => JSON.stringify(_)),
Match.exhaustive,
);

export const InjectSection = (section: string, data: SupportedFormats) => {
const content = formatString(data);
return [`## ${section}`, content].join("\n");
};
44 changes: 44 additions & 0 deletions packages/agents/src/Agent/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { toMarkdownTable } from "../md/utils";
import { InjectSection } from "./injectSection";
import { mapValues } from "radash";

type Section = {
heading: string;
purpose: string;
format: string;
};

const sections = {
planning: {
heading: "Planning",
purpose:
"Use this section to plan your response to the task step by step, and reflect on your approach. It will be discarded. ",
format: "valid markdown",
},
data: {
heading: "Structured Data",
purpose: "Structured data conforming to the provided schema (JSON Schema)",
format: "A json code block containing the valid JSON ",
},
unstructured: {
heading: "Unstructured Data",
purpose:
"Any additional data requested by the instructions which was not part of the structured data. This should rarely be used.",
format: "valid markdown",
},
} satisfies Record<string, Section>;

export const promptContext = [
"Your responses will only be seen by other Agents - so be precise, follow instructions, and format your response carefully.",
"Your response should be in markdown organized into sections by blocks which start with specific Heading Level 2 headings. For example, \n```md\n## MySection \n\n This is my section. \n```\n would be a MySection section with contents 'This is my section' ",
"The table below lists the valid sections, what heading to use, when to use it (purpose), and how to format their contents. ",
toMarkdownTable(Object.values(sections)),
];

export const Prompts = {
sectionKeys: mapValues(sections, section=>section.heading),
responseConventions: InjectSection(
"Response Conventions",
promptContext.join("\n"),
),
};
6 changes: 6 additions & 0 deletions packages/agents/src/agents/HelloAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Agent } from "../Agent/Agent";

export class HelloAgent extends Agent {
name = "Hello Agent";
description = "Welcomes people to the project";
}
6 changes: 6 additions & 0 deletions packages/agents/src/agents/WikiAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Agent } from "../Agent/Agent";

export class WikiAgent extends Agent {
name = "Wikipedia Agent";
description = "Searches wikipedia for background on a given topic or term."
}
13 changes: 13 additions & 0 deletions packages/agents/src/md/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { get, unique } from "radash";

export const toMarkdownTable = <T extends Record<string, any>>(
data: Array<T>
) => {
const keys = unique(data.flatMap((item) => Object.keys(item)));
const headingRow = `| ${keys.join(" | ")} |`;
const separatorRow = `|${keys.map(() => "-----").join("|")}|`;
const dataRows = data
.map((item) => `| ${keys.map((key) => get(item, key, "")).join(" | ")} |`)
.join("\n");
return [headingRow, separatorRow, dataRows].join("\n");
};
10 changes: 9 additions & 1 deletion packages/agents/src/services.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// services.ts

import { Effect, Context, Layer } from "effect";
import { Ollama } from "ollama";
import { ChatRequest, ChatResponse, Message, Ollama } from "ollama";

/**
* LLMService tag
*/
export class LLMService extends Context.Tag("LLMService")<
LLMService,
{
readonly chat: (
req: ChatRequest,
) => Effect.Effect<ChatResponse, Error, never>;
readonly generateText: (
prompt: string,
model: string,
Expand All @@ -28,6 +31,11 @@ export const LLMServiceLive = () => {
return Layer.succeed(
LLMService,
LLMService.of({
chat: (request: ChatRequest) =>
Effect.tryPromise({
try: () => ollama.chat({ ...request, stream: false }),
catch: (err) => new Error('unknown'),
}),
generateText: (prompt: string, model: string) =>
Effect.tryPromise({
try: () =>
Expand Down
16 changes: 16 additions & 0 deletions packages/agents/src/task/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Schema } from "@effect/schema";

export class Task {
constructor(
readonly instructions: string,
readonly acceptanceCriteria: Array<string> = [],
readonly format: Schema.Schema.AnyNoContext,
) {}
}

export class TaskResult<T extends Schema.Schema.Any> {
constructor(
readonly data: Schema.Schema.Type<T>,
readonly unstructured: string,
) {}
}

0 comments on commit a7c6534

Please sign in to comment.