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: Inferable tRPC Adapter #272

Merged
merged 12 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 17 additions & 12 deletions connectors/trpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@ pnpm add @inferable/trpc-connector

## Quick Start

Create your tRPC router as normal:
Create your tRPC router with the Inferable plugin:

```ts
const t = initTRPC.create();
import { inferablePlugin } from "@inferable/trpc-connector";

const router = t.router;
const publicProcedure = t.procedure;
const t = initTRPC.create();
const withInferable = inferablePlugin();

const appRouter = t.router({
userById: publicProcedure
userById: t.procedure
.unstable_concat(withInferable) // It's safe to use unstable_concat - https://trpc.io/docs/faq#unstable
.input(z.object({ id: z.string() }))
.meta({ inferable: true }) // <--- Mark this procedure for Inferable
.meta({ description: "Fetch a user by their ID" }) // This will be used to encrich the LLM context
.query(({ input }) => {
return users.find((user) => user.id === input.id);
}),
Expand Down Expand Up @@ -83,16 +84,20 @@ const result = await client.run({
});
```

## Notes
## Technical Details

The plugin does two things:

1. It adds a `meta` field to the procedures with `{ inferable: { enabled: true } }`. This is used to identify the procedures that should be exposed as Inferable functions.
2. It adds a `ctx` field to the tRPC procedure so you can validate the context that's passed by Inferable in your down stream procedures or middleware.

- Preserve Middleware: All your existing tRPC middleware continues to work
- Type Safety: Maintains full type safety through your tRPC router
- Selective Exposure: Only procedures marked with meta({ inferable: true }) are exposed
- Custom Descriptions: Add descriptions to help guide the AI through meta({ description: "..." })
- This allows you model [human in the loop](https://docs.inferable.ai/pages/human-in-the-loop) workflows where you can fire off a approval request to a human before the function is run.
johnjcsmith marked this conversation as resolved.
Show resolved Hide resolved
- It also allows you to handle [end-user authentication](https://docs.inferable.ai/pages/end-user-authentication) in your tRPC procedures.
Copy link
Contributor

Choose a reason for hiding this comment

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

#281 renames this to custom auth

- For more information on the context object, see the [context documentation](https://docs.inferable.ai/pages/context).

## Documentation

Inferable documentation contains all the information you need to get started with Inferable.
[Inferable documentation](https://docs.inferable.ai) contains all the information you need to get started with Inferable.

## Support

Expand Down
52 changes: 39 additions & 13 deletions connectors/trpc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { AnyRouter, initTRPC } from "@trpc/server";
import { Inferable } from "inferable";
import { RegisteredService } from "inferable/bin/types";
import { ContextInput, RegisteredService } from "inferable/bin/types";

type FunctionConfig = {
path: string;
description: string | undefined;
inputs: any | undefined;
fn: (input: unknown) => any;
fn: (input: unknown, ctx: ContextInput) => any;
};

type Procedure = {
_def?: {
meta?: {
description?: string;
inferable?: boolean;
inferable?: {
enabled: boolean;
additionalContext?: string;
};
};
inputs?: any;
};
Expand All @@ -26,6 +29,18 @@ function camelCase(str: string) {
.join("");
}

export function inferableTRPC() {
const t = initTRPC.context().meta().create();

return {
proc: t.procedure.meta({ inferable: true }).use(async (opts) => {
return opts.next({
ctx: opts.ctx as ContextInput,
});
}),
};
}

export function createInferableService({
name,
router,
Expand All @@ -43,24 +58,35 @@ export function createInferableService({
}): RegisteredService {
const fns: FunctionConfig[] = [];

const caller = createCaller(contextGetter?.() ?? {});

for (const [path, procedure] of Object.entries(router._def.procedures) as [
string,
Procedure
][]) {
if (procedure._def?.meta?.inferable) {
if (typeof caller[path] !== "function") {
throw new Error(
`Procedure ${path} is not a function. Got ${typeof caller[path]}`
);
}

fns.push({
path,
description: procedure._def?.meta?.description,
description:
[
procedure._def?.meta?.description,
procedure._def?.meta?.inferable?.additionalContext,
]
.filter(Boolean)
.join("\n") || undefined,
inputs: procedure._def?.inputs,
fn: caller[path],
fn: async (input: unknown, ctx: ContextInput) => {
const context = contextGetter ? await contextGetter() : {};
const caller = createCaller({ ...ctx, context });

if (typeof caller[path] !== "function") {
throw new Error(
`Procedure ${path} is not a function. Got ${typeof caller[path]}`
);
}

const fn = caller[path] as (input: unknown) => any;

return fn(input);
},
});
}
}
Expand Down
12 changes: 7 additions & 5 deletions connectors/trpc/src/test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { initTRPC } from "@trpc/server";
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";
import { createInferableService } from ".";
import { Inferable } from "inferable";
import { createInferableService, inferableTRPC } from ".";
import { ContextInput, Inferable } from "inferable";
import assert from "assert";

/**
Expand All @@ -23,21 +23,23 @@ const users = [
{ id: "2", name: "Jane Doe", email: "[email protected]" },
];

const plugin = inferableTRPC();

const appRouter = t.router({
"": publicProcedure.query(() => {
return `Inferable TRPC Connector Test v${
require("../package.json").version
}`;
}),
userById: publicProcedure
.unstable_concat(plugin.proc)
.input(z.object({ id: z.string() }))
.meta({ inferable: true })
.query(({ input }) => {
.query(({ input, ctx }) => {
return users.find((user) => user.id === input.id);
}),
users: router({
create: publicProcedure
.meta({ description: "Create a new user", inferable: true })
.unstable_concat(plugin.proc)
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input }) => {
const newUser = { id: (users.length + 1).toString(), ...input };
Expand Down
Loading