diff --git a/.changeset/eleven-needles-fry.md b/.changeset/eleven-needles-fry.md new file mode 100644 index 00000000..505eff99 --- /dev/null +++ b/.changeset/eleven-needles-fry.md @@ -0,0 +1,5 @@ +--- +"@instructor-ai/instructor": minor +--- + +update peer deps + remove baseUrl check on generic client type guard diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index c67ed6b8..287a0471 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -22,7 +22,7 @@ jobs: ANYSCALE_API_KEY: ${{ secrets.ANYSCALE_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78beae35..0f8ad89b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,8 @@ jobs: ANYSCALE_API_KEY: ${{ secrets.ANYSCALE_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} - + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + steps: - uses: actions/checkout@v3 with: diff --git a/.tool-versions b/.tool-versions index 13df5b6c..ce6416c5 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ nodejs 20.9.0 -bun 1.0.15 +bun 1.1.43 python 3.12.1 diff --git a/bun.lockb b/bun.lockb index 1caaa38f..aed4d109 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 94c1ddd3..8a049b43 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "zod": ">=3.23.8" }, "devDependencies": { - "@anthropic-ai/sdk": "0.29.2", + "@anthropic-ai/sdk": "0.33.1", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.1", "@ianvs/prettier-plugin-sort-imports": "4.1.0", @@ -75,8 +75,8 @@ "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-prettier": "^5.1.2", "husky": "^8.0.3", - "llm-polyglot": "2.2.0", - "openai": "4.68.1", + "llm-polyglot": "2.5.0", + "openai": "4.78.1", "prettier": "latest", "ts-inference-check": "^0.3.0", "tsup": "^8.0.1", diff --git a/src/instructor.ts b/src/instructor.ts index 435065dd..f9c749a3 100644 --- a/src/instructor.ts +++ b/src/instructor.ts @@ -475,7 +475,6 @@ function isGenericClient(client: any): client is GenericClient { return ( typeof client === "object" && client !== null && - "baseURL" in client && "chat" in client && typeof client.chat === "object" && "completions" in client.chat && diff --git a/tests/google.test.ts b/tests/google.test.ts new file mode 100644 index 00000000..eca6cc67 --- /dev/null +++ b/tests/google.test.ts @@ -0,0 +1,295 @@ +import Instructor from "@/index" +import { omit } from "@/lib" +import { describe, expect, test } from "bun:test" +import { createLLMClient } from "llm-polyglot" +import z from "zod" + +const googleClient = createLLMClient({ + provider: "google", + apiKey: process.env.GEMINI_API_KEY +}) + +describe("LLMClient google Provider - mode: TOOLS", () => { + const instructor = Instructor({ + client: googleClient, + mode: "TOOLS" + }) + + test("basic completion", async () => { + const completion = await instructor.chat.completions.create({ + model: "gemini-1.5-flash-latest", + max_tokens: 1000, + messages: [ + { + role: "user", + content: "My name is Dimitri Kennedy." + } + ], + response_model: { + name: "extract_name", + schema: z.object({ + name: z.string() + }) + } + }) + + expect(omit(["_meta"], completion)).toEqual({ name: "Dimitri Kennedy" }) + }) + + test("complex schema", async () => { + const completion = await instructor.chat.completions.create({ + model: "gemini-1.5-flash-latest", + max_tokens: 1000, + messages: [ + { + role: "user", + content: `User Data Submission: + + First Name: John + Last Name: Doe + Contact Details: + Email: john.doe@example.com + Phone Number: 555-1234 + Job History: + + Company Name: Acme Corp + Role: Software Engineer + Years: 5 + Company Name: Globex Inc. + Role: Lead Developer + Years: 3 + Skills: + + Programming + Leadership + Communication + ` + } + ], + response_model: { + name: "process_user_data", + schema: z.object({ + story: z + .string() + .describe("A long and mostly made up story about the user - minimum 500 words"), + userDetails: z.object({ + firstName: z.string(), + lastName: z.string(), + contactDetails: z.object({ + email: z.string(), + phoneNumber: z.string().optional() + }) + }), + jobHistory: z.array( + z.object({ + companyName: z.string(), + role: z.string(), + years: z.number().optional() + }) + ), + skills: z.array(z.string()) + }) + } + }) + + expect(omit(["_meta", "story"], completion)).toEqual({ + userDetails: { + firstName: "John", + lastName: "Doe", + contactDetails: { + email: "john.doe@example.com", + phoneNumber: "555-1234" + } + }, + jobHistory: [ + { + companyName: "Acme Corp", + role: "Software Engineer", + years: 5 + }, + { + companyName: "Globex Inc.", + role: "Lead Developer", + years: 3 + } + ], + skills: ["Programming", "Leadership", "Communication"] + }) + }) +}) + +describe("LLMClient google Provider - mode: TOOLS - stream", () => { + const instructor = Instructor({ + client: googleClient, + mode: "TOOLS" + }) + + test("basic completion", async () => { + const completion = await instructor.chat.completions.create({ + model: "gemini-1.5-flash-latest", + stream: true, + max_tokens: 1000, + messages: [ + { + role: "user", + content: "My name is Dimitri Kennedy." + } + ], + response_model: { + name: "extract_name", + schema: z.object({ + name: z.string() + }) + } + }) + + let final = {} + + for await (const result of completion) { + final = result + } + + //@ts-expect-error ignore for testing + expect(omit(["_meta"], final)).toEqual({ name: "Dimitri Kennedy" }) + }) + + test("complex schema", async () => { + const completion = await instructor.chat.completions.create({ + model: "gemini-1.5-flash-latest", + max_tokens: 1000, + stream: true, + messages: [ + { + role: "user", + content: `User Data Submission: + + First Name: John + Last Name: Doe + Contact Details: + Email: john.doe@example.com + Phone Number: 555-1234 + Job History: + + Company Name: Acme Corp + Role: Software Engineer + Years: 5 + Company Name: Globex Inc. + Role: Lead Developer + Years: 3 + Skills: + + Programming + Leadership + Communication + ` + } + ], + response_model: { + name: "process_user_data", + schema: z.object({ + story: z + .string() + .describe("A long and mostly made up story about the user - minimum 500 words"), + userDetails: z.object({ + firstName: z.string(), + lastName: z.string(), + contactDetails: z.object({ + email: z.string(), + phoneNumber: z.string().optional() + }) + }), + jobHistory: z.array( + z.object({ + companyName: z.string(), + role: z.string(), + years: z.number().optional() + }) + ), + skills: z.array(z.string()) + }) + } + }) + + let final = {} + + for await (const result of completion) { + final = result + } + + //@ts-expect-error ignore for testing + expect(omit(["_meta", "story"], final)).toEqual({ + userDetails: { + firstName: "John", + lastName: "Doe", + contactDetails: { + email: "john.doe@example.com", + phoneNumber: "555-1234" + } + }, + jobHistory: [ + { + companyName: "Acme Corp", + role: "Software Engineer", + years: 5 + }, + { + companyName: "Globex Inc.", + role: "Lead Developer", + years: 3 + } + ], + skills: ["Programming", "Leadership", "Communication"] + }) + }) +}) + +describe("LLMClient google Provider - Grounding", () => { + test("grounding with structured output", async () => { + const completion = await googleClient.chat.completions.create({ + model: "gemini-1.5-flash-latest", + messages: [ + { + role: "user", + content: + "You are a food critic. Review the most popular Italian restaurants in Boston's North End, focusing on their traditional dishes." + } + ], + groundingThreshold: 0.7, + max_tokens: 1000, + stream: false + }) + + expect(completion.choices?.[0]?.message?.content).toBeTruthy() + }) + + test("grounding with different thresholds", async () => { + const highThreshold = await googleClient.chat.completions.create({ + model: "gemini-1.5-flash-latest", + messages: [ + { + role: "user", + content: "What are the current top-rated Italian restaurants in Boston's North End?" + } + ], + groundingThreshold: 0.9, + max_tokens: 1000 + }) + + // Lower threshold for more general responses + const lowThreshold = await googleClient.chat.completions.create({ + model: "gemini-1.5-flash-latest", + messages: [ + { + role: "user", + content: "What are the current top-rated Italian restaurants in Boston's North End?" + } + ], + groundingThreshold: 0.5, + max_tokens: 1000 + }) + + expect(highThreshold.choices?.[0]?.message?.content).not.toBe( + lowThreshold.choices?.[0]?.message?.content + ) + }) +})