webfetch.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { describe, expect } from "bun:test"
  2. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  3. import { httpClient } from "@kirincode-ai/core/effect/app-node-platform"
  4. import { Effect, Layer } from "effect"
  5. import { FetchHttpClient, HttpClient } from "effect/unstable/http"
  6. import { Agent } from "../../src/agent/agent"
  7. import { Truncate } from "@/tool/truncate"
  8. import { WebFetchTool } from "../../src/tool/webfetch"
  9. import { SessionID, MessageID } from "../../src/session/schema"
  10. import { Tool } from "@/tool/tool"
  11. import { testEffect } from "../lib/effect"
  12. const it = testEffect(
  13. LayerNode.compile(LayerNode.group([httpClient, Truncate.node, Agent.node]), [
  14. [httpClient, FetchHttpClient.layer as Layer.Layer<HttpClient.HttpClient>],
  15. ]),
  16. )
  17. const ctx = {
  18. sessionID: SessionID.make("ses_test"),
  19. messageID: MessageID.make("msg_message"),
  20. callID: "",
  21. agent: "build",
  22. abort: AbortSignal.any([]),
  23. messages: [],
  24. metadata: () => Effect.void,
  25. ask: () => Effect.void,
  26. }
  27. const withFetch = <A, E, R>(
  28. fetch: (req: Request) => Response | Promise<Response>,
  29. fn: (url: URL) => Effect.Effect<A, E, R>,
  30. ) =>
  31. Effect.acquireUseRelease(
  32. Effect.sync(() => Bun.serve({ port: 0, fetch })),
  33. (server) => fn(server.url),
  34. (server) => Effect.sync(() => server.stop(true)),
  35. )
  36. const exec = Effect.fn("WebFetchToolTest.exec")(function* (args: Tool.InferParameters<typeof WebFetchTool>) {
  37. const info = yield* WebFetchTool
  38. const tool = yield* info.init()
  39. return yield* tool.execute(args, ctx)
  40. })
  41. describe("tool.webfetch", () => {
  42. it.instance("returns image responses as file attachments", () =>
  43. Effect.gen(function* () {
  44. const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
  45. yield* withFetch(
  46. () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
  47. (url) =>
  48. Effect.gen(function* () {
  49. const result = yield* exec({ url: new URL("/image.png", url).toString(), format: "markdown" })
  50. expect(result.output).toBe("Image fetched successfully")
  51. expect(result.attachments).toBeDefined()
  52. expect(result.attachments?.length).toBe(1)
  53. expect(result.attachments?.[0].type).toBe("file")
  54. expect(result.attachments?.[0].mime).toBe("image/png")
  55. expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
  56. expect(result.attachments?.[0]).not.toHaveProperty("id")
  57. expect(result.attachments?.[0]).not.toHaveProperty("sessionID")
  58. expect(result.attachments?.[0]).not.toHaveProperty("messageID")
  59. }),
  60. )
  61. }),
  62. )
  63. it.instance("keeps svg as text output", () =>
  64. withFetch(
  65. () =>
  66. new Response('<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>', {
  67. status: 200,
  68. headers: { "content-type": "image/svg+xml; charset=UTF-8" },
  69. }),
  70. (url) =>
  71. Effect.gen(function* () {
  72. const result = yield* exec({ url: new URL("/image.svg", url).toString(), format: "html" })
  73. expect(result.output).toContain("<svg")
  74. expect(result.attachments).toBeUndefined()
  75. }),
  76. ),
  77. )
  78. it.instance("keeps text responses as text output", () =>
  79. withFetch(
  80. () =>
  81. new Response("hello from webfetch", {
  82. status: 200,
  83. headers: { "content-type": "text/plain; charset=utf-8" },
  84. }),
  85. (url) =>
  86. Effect.gen(function* () {
  87. const result = yield* exec({ url: new URL("/file.txt", url).toString(), format: "text" })
  88. expect(result.output).toBe("hello from webfetch")
  89. expect(result.attachments).toBeUndefined()
  90. }),
  91. ),
  92. )
  93. it.instance("extracts text from html without scripts or styles", () =>
  94. withFetch(
  95. () =>
  96. new Response(
  97. "<html><head><style>.hidden{}</style><script>alert('x')</script></head><body>Hello <b>world</b></body></html>",
  98. {
  99. status: 200,
  100. headers: { "content-type": "text/html; charset=utf-8" },
  101. },
  102. ),
  103. (url) =>
  104. Effect.gen(function* () {
  105. const result = yield* exec({ url: new URL("/page.html", url).toString(), format: "text" })
  106. expect(result.output).toBe("Hello world")
  107. expect(result.attachments).toBeUndefined()
  108. }),
  109. ),
  110. )
  111. })