diff --git a/.changeset/add-policycheck-provider.md b/.changeset/add-policycheck-provider.md new file mode 100644 index 000000000..dbb1f1897 --- /dev/null +++ b/.changeset/add-policycheck-provider.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": minor +--- + +Added PolicyCheck action provider for pre-purchase seller policy verification. Enables AI agents to analyze e-commerce seller return policies, shipping terms, warranty coverage, and terms of service before making purchases. Returns risk level, buyer protection score, key findings, and purchase recommendation. Walletless provider — works on all networks. diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 7f2b0233a..1231fd5c5 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -22,6 +22,7 @@ export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; export * from "./opensea"; +export * from "./policycheck"; export * from "./spl"; export * from "./superfluid"; export * from "./sushi"; diff --git a/typescript/agentkit/src/action-providers/policycheck/index.ts b/typescript/agentkit/src/action-providers/policycheck/index.ts new file mode 100644 index 000000000..6b9c30678 --- /dev/null +++ b/typescript/agentkit/src/action-providers/policycheck/index.ts @@ -0,0 +1,2 @@ +export * from "./policycheckActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.test.ts b/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.test.ts new file mode 100644 index 000000000..37be30534 --- /dev/null +++ b/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.test.ts @@ -0,0 +1,296 @@ +import { policycheckActionProvider, PolicyCheckActionProvider } from "./policycheckActionProvider"; + +// Mock A2A responses +const MOCK_LOW_RISK_RESPONSE = { + jsonrpc: "2.0", + id: "1", + result: { + id: "task_test_1", + status: { state: "completed" }, + artifacts: [ + { + artifactId: "artifact_test_1", + name: "policy_analysis", + parts: [ + { + kind: "data", + data: { + riskLevel: "low", + buyerProtectionScore: 85, + keyFindings: [ + "30-day return policy with free return shipping", + "1-year manufacturer warranty included", + "Full refund to original payment method", + ], + summary: "Strong buyer protections detected. 30-day return window, free return shipping, 1-year manufacturer warranty.", + }, + mimeType: "application/json", + }, + { + kind: "text", + text: "Strong buyer protections across all policy categories. 30-day return window, free return shipping, 1-year warranty.", + }, + ], + }, + ], + messages: [], + }, +}; + +const MOCK_HIGH_RISK_RESPONSE = { + jsonrpc: "2.0", + id: "2", + result: { + id: "task_test_2", + status: { state: "completed" }, + artifacts: [ + { + artifactId: "artifact_test_2", + name: "policy_analysis", + parts: [ + { + kind: "data", + data: { + riskLevel: "high", + buyerProtectionScore: 25, + keyFindings: [ + "No return policy found", + "Binding arbitration clause detected", + "Liability cap limits seller responsibility to purchase price", + ], + summary: "High risk indicators detected. 3 of 5 policy categories flagged. Binding arbitration limits dispute resolution. No return policy found.", + }, + mimeType: "application/json", + }, + { + kind: "text", + text: "High risk indicators detected. 3 of 5 policy categories flagged. Binding arbitration limits dispute resolution.", + }, + ], + }, + ], + messages: [], + }, +}; + +const MOCK_ERROR_RESPONSE = { + jsonrpc: "2.0", + id: "3", + error: { + code: -32000, + message: "Analysis failed: unable to fetch policy page", + }, +}; + +describe("PolicyCheckActionProvider", () => { + const fetchMock = jest.fn(); + global.fetch = fetchMock; + + let provider: PolicyCheckActionProvider; + + beforeEach(() => { + jest.resetAllMocks().restoreAllMocks(); + provider = policycheckActionProvider(); + }); + + describe("constructor", () => { + it("should use default API URL when no config provided", () => { + const p = policycheckActionProvider(); + expect(p["apiUrl"]).toBe("https://policycheck.tools/api/a2a"); + }); + + it("should use custom API URL from config", () => { + const p = policycheckActionProvider({ apiUrl: "https://custom.api/a2a" }); + expect(p["apiUrl"]).toBe("https://custom.api/a2a"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for any network", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet" } as any)).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "solana", networkId: "mainnet" } as any)).toBe(true); + }); + }); + + describe("analyze", () => { + it("should return low-risk assessment for safe seller policies", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_LOW_RISK_RESPONSE, + }); + + const result = await provider.analyze({ + policyText: + "30-day return policy. Free return shipping. Full refund within 5 business days. 1-year warranty.", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(true); + expect(parsed.riskLevel).toBe("low"); + expect(parsed.buyerProtectionScore).toBe(85); + expect(parsed.summary).toContain("Strong buyer protections"); + expect(parsed.keyFindings).toHaveLength(3); + expect(parsed.analyzedUrl).toBe("direct text analysis"); + }); + + it("should return high-risk assessment for risky seller policies", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_HIGH_RISK_RESPONSE, + }); + + const result = await provider.analyze({ + policyText: "All sales final. Binding arbitration. Liability capped at $10.", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(true); + expect(parsed.riskLevel).toBe("high"); + expect(parsed.buyerProtectionScore).toBe(25); + expect(parsed.summary).toContain("High risk indicators"); + }); + + it("should send seller URL as data part with quick-risk-check skill", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_LOW_RISK_RESPONSE, + }); + + const result = await provider.analyze({ + sellerUrl: "https://example-store.com", + }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, options] = fetchMock.mock.calls[0]; + expect(url).toBe("https://policycheck.tools/api/a2a"); + + const body = JSON.parse(options.body); + expect(body.method).toBe("message/send"); + expect(body.params.message.parts[0]).toEqual( + expect.objectContaining({ + kind: "data", + data: { seller_url: "https://example-store.com", skill: "quick-risk-check" }, + }), + ); + + const parsed = JSON.parse(result); + expect(parsed.success).toBe(true); + expect(parsed.analyzedUrl).toBe("https://example-store.com"); + }); + + it("should send policy text as text part", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_LOW_RISK_RESPONSE, + }); + + await provider.analyze({ policyText: "Test policy text" }); + + const [, options] = fetchMock.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.params.message.parts[0]).toEqual( + expect.objectContaining({ + kind: "text", + text: "Test policy text", + }), + ); + }); + + it("should return error for API error response", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_ERROR_RESPONSE, + }); + + const result = await provider.analyze({ + sellerUrl: "https://broken-site.com", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("unable to fetch policy page"); + }); + + it("should return error for HTTP failure", async () => { + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 500, + }); + + const result = await provider.analyze({ + policyText: "Some policy text", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("HTTP 500"); + }); + + it("should return error for network failure", async () => { + fetchMock.mockRejectedValueOnce(new Error("Network timeout")); + + const result = await provider.analyze({ + policyText: "Some policy text", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("Network timeout"); + }); + + it("should return error when no analysis data in response", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + jsonrpc: "2.0", + id: "4", + result: { id: "task_empty", status: { state: "completed" }, artifacts: [], messages: [] }, + }), + }); + + const result = await provider.analyze({ + policyText: "Some policy text", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(false); + expect(parsed.error).toContain("No analysis data"); + }); + + it("should include summary text when available", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_LOW_RISK_RESPONSE, + }); + + const result = await provider.analyze({ + policyText: "Good policy text", + }); + const parsed = JSON.parse(result); + + expect(parsed.summary).toContain("Strong buyer protections"); + }); + }); + + describe("checkUrl", () => { + it("should delegate to analyze with sellerUrl", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => MOCK_LOW_RISK_RESPONSE, + }); + + const result = await provider.checkUrl({ + sellerUrl: "https://example-store.com", + }); + const parsed = JSON.parse(result); + + expect(parsed.success).toBe(true); + expect(parsed.analyzedUrl).toBe("https://example-store.com"); + + // Verify it called the A2A API with quick-risk-check skill + const [, options] = fetchMock.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.params.message.parts[0].data.skill).toBe("quick-risk-check"); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.ts b/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.ts new file mode 100644 index 000000000..77b84a4aa --- /dev/null +++ b/typescript/agentkit/src/action-providers/policycheck/policycheckActionProvider.ts @@ -0,0 +1,202 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { PolicyCheckAnalyzeSchema, PolicyCheckUrlSchema } from "./schemas"; + +const POLICYCHECK_A2A_URL = "https://policycheck.tools/api/a2a"; + +/** + * Configuration options for the PolicyCheck action provider. + */ +export interface PolicyCheckConfig { + /** + * Override the default PolicyCheck API URL. + */ + apiUrl?: string; +} + +/** + * PolicyCheckActionProvider provides seller policy risk intelligence for AI agents + * involved in e-commerce. It analyzes return policies, shipping terms, warranty + * coverage, and terms of service to produce risk data including risk level + * classifications, buyer protection scores, and key findings. + * + * The agent should use this data alongside other context (purchase amount, buyer + * risk tolerance, item category) to make its own purchase decisions. + * + * This is a walletless action provider — no wallet is required. + */ +export class PolicyCheckActionProvider extends ActionProvider { + private readonly apiUrl: string; + + /** + * Constructs a new PolicyCheckActionProvider. + * + * @param config - Optional configuration. + */ + constructor(config?: PolicyCheckConfig) { + super("policycheck", []); + this.apiUrl = config?.apiUrl || POLICYCHECK_A2A_URL; + } + + /** + * Get seller policy risk data to inform purchase decisions. + * + * @param args - The input arguments (policyText and/or sellerUrl). + * @returns A string containing the risk data with scores, findings, and factual summary. + */ + @CreateAction({ + name: "policycheck_analyze", + description: `Get seller policy risk data to inform purchase decisions. Analyzes return policies, shipping terms, warranty coverage, and terms of service. Returns risk level (low/medium/high/critical), buyer protection score (0-100), key findings about specific policy issues, and a factual summary. The agent should use this data alongside other context (purchase amount, buyer risk tolerance, item category) to make its own purchase decision. + +Inputs: +- policyText: The full text of the seller's policy to analyze. Provide this OR sellerUrl. +- sellerUrl: The URL of the e-commerce store. The service will find and analyze policies automatically. Provide this OR policyText. + +Risk factors detected include: +- Missing or restrictive return policies +- Binding arbitration clauses affecting dispute resolution options +- Liability caps limiting seller responsibility +- Missing warranty information +- Restocking fees or buyer-pays-return-shipping terms + +A buyer protection score below 50 indicates limited policy protections. Binding arbitration clauses affect dispute resolution options. Missing return policies are notable risk factors.`, + schema: PolicyCheckAnalyzeSchema, + }) + async analyze(args: z.infer): Promise { + try { + // Build A2A message parts based on input type + const parts: Array> = []; + + if (args.sellerUrl) { + parts.push({ + kind: "data", + data: { seller_url: args.sellerUrl, skill: "quick-risk-check" }, + mimeType: "application/json", + }); + parts.push({ + kind: "text", + text: `Quick risk check on ${args.sellerUrl}`, + }); + } else if (args.policyText) { + parts.push({ + kind: "text", + text: args.policyText, + }); + } + + const response = await fetch(this.apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "message/send", + params: { + message: { + role: "user", + parts, + }, + }, + id: Date.now().toString(), + }), + }); + + if (!response.ok) { + return JSON.stringify({ + success: false, + error: `PolicyCheck API returned HTTP ${response.status}`, + }); + } + + const data = await response.json(); + + if (data.error) { + return JSON.stringify({ + success: false, + error: data.error.message || "PolicyCheck analysis failed", + }); + } + + // Extract analysis from A2A response artifacts + const result = data.result; + const artifacts = result?.artifacts || []; + let analysisData: Record | null = null; + let summaryText = ""; + + for (const artifact of artifacts) { + for (const part of artifact.parts || []) { + if (part.kind === "data" && part.data) { + analysisData = part.data; + } else if (part.kind === "text" && part.text) { + summaryText = part.text; + } + } + } + + if (analysisData) { + return JSON.stringify({ + success: true, + riskLevel: analysisData.riskLevel, + buyerProtectionScore: analysisData.buyerProtectionScore, + keyFindings: analysisData.keyFindings, + summary: (analysisData.summary as string) || summaryText || undefined, + analyzedUrl: args.sellerUrl || "direct text analysis", + }); + } + + // Fallback: return text summary if no structured data + if (summaryText) { + return JSON.stringify({ + success: true, + summary: summaryText, + analyzedUrl: args.sellerUrl || "direct text analysis", + }); + } + + return JSON.stringify({ + success: false, + error: "No analysis data returned from PolicyCheck", + }); + } catch (error) { + return JSON.stringify({ + success: false, + error: `PolicyCheck request failed: ${error instanceof Error ? error.message : String(error)}`, + }); + } + } + + /** + * Quick URL-based seller check. + * + * @param args - The seller URL to check. + * @returns A string containing the risk assessment. + */ + @CreateAction({ + name: "policycheck_check_url", + description: `Quick seller policy risk check by URL. Provide the store URL and the service will find and analyze the seller's policies automatically. Returns risk level, buyer protection score, key findings, and a factual summary. + +Inputs: +- sellerUrl: The URL of the e-commerce store to check (e.g., 'https://example-store.com').`, + schema: PolicyCheckUrlSchema, + }) + async checkUrl(args: z.infer): Promise { + return this.analyze({ sellerUrl: args.sellerUrl }); + } + + /** + * Checks if this provider supports the given network. + * PolicyCheck is walletless and works on all networks. + * + * @returns Always true. + */ + supportsNetwork = () => true; +} + +/** + * Factory function to create a PolicyCheckActionProvider instance. + * + * @param config - Optional configuration. + * @returns A new PolicyCheckActionProvider instance. + */ +export const policycheckActionProvider = (config?: PolicyCheckConfig) => + new PolicyCheckActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/policycheck/schemas.ts b/typescript/agentkit/src/action-providers/policycheck/schemas.ts new file mode 100644 index 000000000..d94f3f347 --- /dev/null +++ b/typescript/agentkit/src/action-providers/policycheck/schemas.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +/** + * Input schema for analyzing seller policies. + */ +export const PolicyCheckAnalyzeSchema = z + .object({ + policyText: z + .string() + .optional() + .describe( + "The full text of the seller's return policy, shipping policy, warranty, or terms of service to analyze. Provide this OR sellerUrl.", + ), + sellerUrl: z + .string() + .optional() + .describe( + "The URL of the e-commerce store to check (e.g., 'https://example-store.com'). The service will find and analyze the seller's policies automatically. Provide this OR policyText.", + ), + }) + .strict() + .refine(data => data.policyText || data.sellerUrl, { + message: "Either policyText or sellerUrl must be provided", + }); + +/** + * Input schema for quick URL-based seller check. + */ +export const PolicyCheckUrlSchema = z + .object({ + sellerUrl: z + .string() + .describe( + "The URL of the e-commerce store to check (e.g., 'https://example-store.com'). The service will find and analyze the seller's policies automatically.", + ), + }) + .strict();