Skip to content

Instantly share code, notes, and snippets.

@laginha
Last active August 14, 2024 11:19
Show Gist options
  • Save laginha/7807e3ab32a918e6d352a5981ab250c7 to your computer and use it in GitHub Desktop.
Save laginha/7807e3ab32a918e6d352a5981ab250c7 to your computer and use it in GitHub Desktop.
Yet incomplete alternative to dify-client made for fun
import { fetchEventData, IFetchOptions } from 'fetch-sse'
// Defines the options for running a workflow, including an optional data handler.
export interface FetchEventStreamOptions extends IFetchOptions {
onData?: (json: any) => void
}
// Extends FetchEventStreamOptions to include user-specific and workflow control options.
export interface WorkflowOptions extends FetchEventStreamOptions {
body: {
user: string // User identifier.
inputs: any // Inputs for the workflow.
}
}
// Base API URL for DIFY services.
const DIFY_API_URL = 'https://api.dify.ai/v1'
// Endpoint URL for running workflows.
const WORKFLOW_URL = `${DIFY_API_URL}/workflows/run`
// Defaults for the fetch request.
const DEFAULT_METHOD = 'POST'
const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json',
}
/**
* Initiates an EventSource connection to stream data from a specified URL.
* This function sets up a connection using the FetchEventSource API and handles incoming messages.
*
* @param {string} url - The URL to connect to for streaming data.
* @param {FetchEventStreamOptions} options - Configuration options for the event source connection,
* including event handlers and fetch initialization options.
* @returns {Promise<void>} - A promise that resolves when the event source connection is established.
*/
export function fetchEventStream(
url: string,
options: FetchEventStreamOptions
): Promise<void> {
const { onData, ...fetchEventDataInit } = options
return fetchEventData(url, {
/**
* Default handler for incoming messages from the event source.
* Filters out 'ping' events and processes other messages by attempting to parse them as JSON.
* If parsing is successful and an 'ondata' handler is provided, it passes the parsed data to the handler.
*/
onMessage(event) {
if (event?.data) {
try {
onData?.(JSON.parse(event.data))
} catch (error) {
console.error('Error parsing event data:', error)
}
}
},
...fetchEventDataInit,
})
}
/**
* Base class for clients interacting with the DIFY API.
*/
class BaseClient {
/**
* Default options for fetch requests, including headers and HTTP method.
*/
protected defaultOptions: any
/**
* Constructs a new BaseClient instance with default options initialized.
* @param {string} apiKey - The API key used for authorization.
*/
constructor(apiKey: string) {
this.defaultOptions = {
method: DEFAULT_METHOD, // Default HTTP method for requests.
headers: {
...DEFAULT_HEADERS,
Authorization: `Bearer ${apiKey}`, // Authorization header using Bearer token.
},
}
}
}
/**
* Client for managing blocking workflows via the DIFY API.
*/
export class BlockClient extends BaseClient {
private responseMode: string = 'blocking'
/**
* Constructs a new BlockClient instance.
* @param {string} apiKey - The API key used for authorization.
*/
constructor(apiKey: string) {
super(apiKey)
}
}
/**
* Client for managing streaming workflows via the DIFY API.
*/
export class StreamClient extends BaseClient {
private responseMode: string = 'streaming'
/**
* Constructs a new StreamClient instance.
* @param {string} apiKey - The API key used for authorization.
*/
constructor(apiKey: string) {
super(apiKey)
}
/**
* Executes a workflow with the provided options.
* @param {WorkflowOptions} options - The options for the workflow execution.
* @returns {Promise<void>} A promise that resolves when the workflow is executed.
*/
runWorkflow(options: WorkflowOptions): Promise<void> {
const { body, ...runWorkflowOptions } = options
return fetchEventStream(WORKFLOW_URL, {
data: {
...body,
response_mode: this.responseMode, // Response mode set to 'streaming'.
},
...this.defaultOptions,
...runWorkflowOptions,
})
}
}
@laginha
Copy link
Author

laginha commented Jul 31, 2024

npm i fetch-sse

Example usage:

const client = new StreamClient('DIFY_API_KEY')

client.runWorkflow({
  body: {
    inputs: { input_name: 'foobar' },
    user: 'user-id',
  },
  onData({ event, data }) {
    if (event === 'text_chunk') {
      console.log(data.text)
    }
  }
})

@laginha
Copy link
Author

laginha commented Aug 2, 2024

Nextjs' App Route Handler example:

/app/workflow/route.ts

const dify = new StreamClient(process.env.DIFY_SECRET_KEY)
const encoder = new TextEncoder()

const RESPONSE_HEADERS = {
  'Content-Type': 'text/event-stream; charset=utf-8',
  'Cache-Control': 'no-cache, no-transform',
  Connection: 'keep-alive',
}

export async function POST(request: Request) {
  const { inputs } = await request.json()
  const responseStream = new ReadableStream({
    async start(controller) {
      await dify.runWorkflow({
        body: {
          inputs,
          user: 'app-route-handler',
        },
        onMessage(event) {
          if (event?.data) {
            const chunk = encoder.encode('data: ' + event.data + '\n\n')
            controller.enqueue(chunk)
          }
        },
        onError() {
          controller.close()
        },
        onClose() {
          controller.close()
        },
      })
    },
  })

  return new Response(responseStream, {
    headers: RESPONSE_HEADERS,
  })
}


export const runtime = 'edge'
export const dynamic = 'force-dynamic'

client side

await fetchEventStream('/workflow', {
  method: 'POST',
  data: {
    inputs: { foo: 'bar' }
  },
  onData(data) {
    console.log(data)
  },
})

@laginha
Copy link
Author

laginha commented Aug 9, 2024

Using dify-client instead on the route handler:

const client = new CompletionClient(process.env.DIFY_SECRET_KEY)

const RESPONSE_HEADERS = {
  'Content-Type': 'text/event-stream; charset=utf-8',
  'Cache-Control': 'no-cache, no-transform',
  Connection: 'keep-alive',
}

export async function POST(request: Request) {
  const { inputs } = await request.json()
  // @ts-ignore <-- don't foget this
  const res = await client.runWorkflow(inputs, 'app-route-handler', true)
  return new Response(res.data as any, { headers: RESPONSE_HEADERS })
}

Client side using fetch-sse

fetchEventData('/protected/workflow', {
  method: 'POST',
  data: {
    inputs: { foo: 'bar' }
  },
  onMessage(event) {
    if (event?.data) {
      console.log(JSON.parse(event.data))
    }
  }
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment