weifuwu 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
8
8
 
9
- Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI, workflow — are standalone modules you import and mount with `app.use()`.
9
+ Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI — are standalone modules you import and mount with `app.use()`.
10
10
 
11
11
  ## Features
12
12
 
@@ -18,8 +18,8 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
18
18
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
19
  - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
20
  - **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
21
- - **AI workflows** — `workflow(handler)` sub-Router intent-to-execution pipelines with `tool()` + SSE
22
- - **AI Agent** — `agent()` — server-side AI agents with chat/workflow/knowledge types, OpenAI-compatible, Ollama-ready
21
+ - **DAG workflow tool** — `runWorkflow()` — multi-step execution engine as a single AI SDK `Tool`
22
+ - **AI Agent** — `agent()` — server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
23
23
  - **Messaging** — `messager()` — real-time chat with channels, WebSocket, agent routing, webhook support
24
24
  - **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
25
25
  - **Redis** — `redis()` — ioredis client, `ctx.redis`, middleware
@@ -278,6 +278,50 @@ await users.createIndex('embedding', { // pgvector HNSW
278
278
  await users.drop({ cascade: true })
279
279
  ```
280
280
 
281
+ ### Type-safe CRUD with BoundTable
282
+
283
+ Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
284
+
285
+ ```ts
286
+ // pg.table() — auto-binds sql, no need to pass it
287
+ const users = pg.table('_users', {
288
+ id: serial('id').primaryKey(),
289
+ name: text('name').notNull(),
290
+ email: text('email').unique(),
291
+ active: boolean('active').default(true),
292
+ createdAt: timestamptz('created_at').default(sql`NOW()`),
293
+ })
294
+
295
+ // INSERT ... RETURNING * — auto-strips serial id
296
+ const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
297
+ // → { id: 1, name: 'Alice', email: 'alice@test.com', active: true, ... }
298
+
299
+ // SELECT ... WHERE id = ? LIMIT 1
300
+ const found = await users.findById(1)
301
+
302
+ // SELECT ... WHERE ... [ORDER BY ...] [LIMIT ...] [OFFSET ...]
303
+ const admins = await users.find({ role: 'admin' })
304
+ const sorted = await users.find({ active: true }, { orderBy: { name: 'asc' } })
305
+ const page = await users.find(undefined, { limit: 10, offset: 0 })
306
+ const filtered = await users.find({ role: 'admin' }, { orderBy: { name: 'desc' }, limit: 5 })
307
+
308
+ // UPDATE ... SET ... WHERE ... RETURNING *
309
+ const updated = await users.update({ id: 1 }, { name: 'Bob' })
310
+ // With SQL expressions:
311
+ await users.update({ id: 1 }, { name: 'Bob', updated_at: sql`NOW()` })
312
+
313
+ // DELETE ... WHERE ... RETURNING 1
314
+ const ok = await users.delete({ id: 1 })
315
+ ```
316
+
317
+ When using `pgTable()` directly (without `pg`), pass `sql` as the first argument:
318
+
319
+ ```ts
320
+ const t = pgTable('_users', { ... })
321
+ await t.insert(ctx.sql, { name: 'Alice' })
322
+ await t.find(ctx.sql, { role: 'admin' }, { orderBy: { name: 'asc' } })
323
+ ```
324
+
281
325
  ### Complex queries use raw SQL
282
326
 
283
327
  ```ts
@@ -690,7 +734,7 @@ await fetch('http://localhost/api/sys/tenants/invite', {
690
734
 
691
735
  ## AI Agent
692
736
 
693
- Server-side AI agents with OpenAI-compatible API. Built-in chat, workflow (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
737
+ Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
694
738
 
695
739
  ```ts
696
740
  import { agent } from 'weifuwu'
@@ -704,7 +748,7 @@ app.use('/api', agents.router())
704
748
  | Type | Description | Execution |
705
749
  |------|-------------|-----------|
706
750
  | `chat` | Pure conversation | `streamText()` / `generateText()` |
707
- | `workflow` | Tool-calling agent | `streamText({ tools })` |
751
+ | `tool-use` | Tool-calling agent | `streamText({ tools })` |
708
752
 
709
753
  ### Knowledge (RAG)
710
754
 
@@ -841,18 +885,18 @@ app.use('/chat', ai(async (req, ctx) => {
841
885
  serve(app.handler(), { port: 3000 })
842
886
  ```
843
887
 
844
- ## Workflow
888
+ ## runWorkflow
845
889
 
846
- Define business capabilities as **Tools** (`tool()`), then chain them into **workflows** for AI-driven multi-step execution. Works with or without an LLM hand-write the workflow JSON or let AI generate it from a goal.
890
+ Multi-step DAG execution engine packaged as a single AI SDK `Tool`. Use it with `streamText()` or `generateText()` when the LLM needs conditional logic, loops, or multi-step tool orchestration.
847
891
 
848
892
  ```ts
849
- import { Router, tool, workflow } from 'weifuwu'
893
+ import { tool, streamText } from 'ai'
894
+ import { runWorkflow } from 'weifuwu'
850
895
  import { z } from 'zod'
851
896
 
852
- // 1. Define tools (business capabilities)
853
897
  const tools = {
854
898
  queryUser: tool({
855
- description: 'Query user info, returns email, name',
899
+ description: 'Query user info',
856
900
  inputSchema: z.object({ userId: z.string() }),
857
901
  execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
858
902
  }),
@@ -861,139 +905,57 @@ const tools = {
861
905
  inputSchema: z.object({ to: z.string(), subject: z.string() }),
862
906
  execute: async ({ to, subject }) => ({ sent: true }),
863
907
  }),
908
+ runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
864
909
  }
865
910
 
866
- // 2. Mount workflow sub-router
867
- const app = new Router()
868
- app.use('/agent', workflow(() => ({ tools })))
869
- // POST /agent { nodes: [...] } → 200 { workflow: {...}, result: ... }
870
-
871
- // With SSE streaming:
872
- app.use('/agent-stream', workflow(() => ({ tools, stream: true })))
873
- // POST /agent-stream { nodes: [...] }
874
- // → 200 { workflowId: "xxx", eventsUrl: "/xxx/events" }
875
- // GET /agent-stream/:workflowId/events
876
- // → SSE: workflow-start → node-start → node-end → complete
877
-
878
- // With LLM model (generates workflow from goal):
879
- app.use('/agent-llm', workflow(() => ({
911
+ // Use in any streamText call — the LLM can decide when to trigger a workflow
912
+ const result = await streamText({
913
+ model,
880
914
  tools,
881
- model: openai('gpt-4o'),
882
- })))
883
- // POST /agent-llm { goal: "给用户123发欢迎邮件" }
884
- // ← LLM generates → executes → returns result
885
- ```
886
-
887
- ### Tool
888
-
889
- ```ts
890
- import { tool } from 'weifuwu'
891
- import { z } from 'zod'
892
-
893
- const myTool = tool({
894
- description: '做什么的,返回什么',
895
- inputSchema: z.object({ key: z.string() }),
896
- execute: async (input, ctx) => {
897
- return { result: input.key }
898
- },
899
- })
900
- ```
901
-
902
- `ctx.onStream` 用于流式推送(如 LLM token 输出):
903
-
904
- ```ts
905
- const llmTool = tool({
906
- description: '生成文本',
907
- inputSchema: z.object({ prompt: z.string() }),
908
- execute: async (input, ctx) => {
909
- const stream = await openai.chat.completions.create({ ... })
910
- let full = ''
911
- for await (const chunk of stream) {
912
- full += chunk.choices[0]?.delta?.content || ''
913
- ctx.onStream?.({ type: 'llm-stream', chunk, accumulated: full })
914
- }
915
- return { text: full }
916
- },
915
+ messages: [{ role: 'user', content: '查询用户123,如果存在则发送欢迎邮件' }],
917
916
  })
918
917
  ```
919
918
 
920
- ### Core Nodes
919
+ ### Node types
921
920
 
922
- 7 built-in node types:
921
+ 7 built-in node types for defining the execution graph:
923
922
 
924
923
  | Node | Purpose | Input |
925
924
  |------|---------|-------|
926
- | `call` | Call a tool or sub-workflow | `{ tool: "name", args: {...} }` or `{ function: "name", args: {...} }` |
927
- | `set` | Declare or assign a variable | `{ name: "x", value: 42 }` |
925
+ | `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
926
+ | `set` | Assign a variable | `{ name: "x", value: 42 }` |
928
927
  | `get` | Read a variable | `{ name: "x" }` |
929
928
  | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
930
929
  | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
931
930
  | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
932
931
  | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
933
932
 
934
- ### Variable Reference Syntax
933
+ ### Reference syntax
935
934
 
936
935
  | Pattern | Meaning | Example |
937
936
  |---------|---------|---------|
938
937
  | `$var.x` | Variable `x` | `$var.counter` |
939
938
  | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
940
939
  | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
941
- | `$input.userId` | Workflow input param | `$input.userId` |
942
- | `42`, `true`, `"hello"` | Literal values | Passed as-is |
943
-
944
- ### Engine API
945
-
946
- For programmatic use outside of Router:
947
-
948
- ```ts
949
- import { createWorkflowEngine, createSSEManager } from 'weifuwu'
950
-
951
- const sse = createSSEManager()
952
- const engine = createWorkflowEngine({ tools, sseManager: sse })
953
-
954
- // Sync execution
955
- const result = await engine.execute({ nodes: [...] })
940
+ | `$input.userId` | Input param | `$input.userId` |
956
941
 
957
- // Async execution with SSE
958
- engine.runAsync('wf-1', { nodes: [...] })
959
- ```
942
+ ### LLM generation
960
943
 
961
- ### SSE Events
944
+ Pass a `model` to `runWorkflow` — the LLM generates the workflow JSON from a goal:
962
945
 
963
946
  ```ts
964
- const sse = createSSEManager()
965
- const stream = sse.createStream('wf-1')
947
+ const runWF = runWorkflow({
948
+ tools: { queryUser, sendEmail },
949
+ model: openai('gpt-4o'),
950
+ })
966
951
 
967
- const reader = stream.getReader()
968
- // event: workflow-start — { workflowId, goal }
969
- // event: node-start — { nodeId, tool, input }
970
- // event: node-end — { nodeId, output }
971
- // event: llm-stream — { nodeId, chunk, accumulated }
972
- // event: complete — { result, duration }
973
- // event: error — { error }
952
+ const result = await streamText({
953
+ model,
954
+ tools: { runWF },
955
+ })
974
956
  ```
975
957
 
976
- ### Sub-workflows
977
-
978
- Define reusable sub-workflows in the `functions` field:
979
-
980
- ```json
981
- {
982
- "functions": {
983
- "double": {
984
- "inputSchema": { "type": "object", "properties": { "x": { "type": "number" } } },
985
- "workflow": {
986
- "nodes": [
987
- { "id": "calc", "tool": "eval", "input": { "expression": "$input.x * 2" } }
988
- ]
989
- }
990
- }
991
- },
992
- "nodes": [
993
- { "id": "call_double", "tool": "call", "input": { "function": "double", "args": { "x": 21 } } }
994
- ]
995
- }
996
- ```
958
+ The LLM calls `runWF` with a goal, and `runWorkflow` internally calls `generateText` to produce the workflow nodes, then executes them.
997
959
 
998
960
  ## React pages with tsx()
999
961
 
@@ -1194,14 +1156,12 @@ export default function NotFound() {
1194
1156
  ## Usage within a full app
1195
1157
 
1196
1158
  ```ts
1197
- import { serve, Router, ai, graphql, workflow } from 'weifuwu'
1198
- import { tsx } from 'weifuwu/tsx'
1159
+ import { serve, Router, ai, graphql } from 'weifuwu'
1199
1160
 
1200
1161
  const app = new Router()
1201
1162
  app.use('/', await tsx({ dir: './pages/' }))
1202
1163
  app.use('/chat', ai(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages })))
1203
1164
  app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } })))
1204
- app.use('/agent', workflow(() => ({ tools: myTools, stream: true })))
1205
1165
  app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
1206
1166
 
1207
1167
  serve(app.handler(), { websocket: app.websocketHandler() })
@@ -1334,7 +1294,7 @@ Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
1334
1294
  | `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
1335
1295
  | `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
1336
1296
  | `embeddingDimension` | `1024` | Vector dimension for pgvector |
1337
- | `tools` | — | Tools for workflow-type agents (ai SDK `Tool` objects) |
1297
+ | `tools` | — | Tools for tool-use agents (ai SDK `Tool` objects) |
1338
1298
 
1339
1299
  Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
1340
1300
 
@@ -1418,12 +1378,12 @@ serve(app.handler(), { websocket: app.websocketHandler() })
1418
1378
  | `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
1419
1379
  | `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
1420
1380
  | `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
1421
- | `agent(options)` | AI Agent — chat/workflow/knowledge, Ollama-ready, programmatic API |
1381
+ | `agent(options)` | AI Agent — chat/tool-use/knowledge, Ollama-ready, programmatic API |
1422
1382
  | `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
1423
1383
  | `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
1424
1384
  | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
1425
1385
  | `ai(handler)` | AI streaming endpoint (POST) |
1426
- | `workflow(handler)` | Workflow engine (POST + SSE) |
1386
+ | `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
1427
1387
 
1428
1388
  ### Deploy
1429
1389
 
@@ -1441,13 +1401,14 @@ serve(app.handler(), { websocket: app.websocketHandler() })
1441
1401
  | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
1442
1402
  | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
1443
1403
  | `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
1444
- | `createWorkflowEngine(options)` | Programmatic workflow engine |
1445
- | `createSSEManager()` | SSE event manager for workflows |
1446
- | `tool(def)` | Define a workflow tool |
1447
- | `pgTable(name, columns)` | Type-safe table schema definition with DDL generation |
1404
+ | `runWorkflow(options)` | Create a DAG execution AI SDK `Tool` — `{ tools?, model?, maxSteps? }` |
1405
+ | `pgTable(name, columns)` | Type-safe table schema definition with DDL + CRUD |
1406
+ | `pg.table(name, columns)` | Pre-bound table (no `sql` parameter needed for CRUD) |
1448
1407
  | `serial()`, `uuid()`, `text()`, `integer()`, `boolean()`, `timestamptz()`, `jsonb()`, `textArray()`, `vector()` | Column type builders |
1449
- | `sql(strings, ...)` | SQL expression literal for table defaults (e.g. `sql\`NOW()\``) |
1408
+ | `sql(strings, ...)` | SQL expression literal for defaults and SET values (e.g. `sql\`NOW()\``) |
1450
1409
  | `PgModule` | Base class for database-backed modules (provides `sql`, `close()`) |
1410
+ | `BoundTable` | Table with pre-bound `sql` — returned by `pg.table()` |
1411
+ | `FindOptions` | Query options: `{ orderBy?, limit?, offset? }` for `find()` |
1451
1412
 
1452
1413
  Import `useTsx` and `TsxContext` from `'weifuwu'`.
1453
1414
 
@@ -1,4 +1,4 @@
1
- import type { LanguageModel, EmbeddingModel, Tool } from '../vendor.ts';
1
+ import { type LanguageModel, type EmbeddingModel, type Tool } from 'ai';
2
2
  import type { Sql } from '../vendor.ts';
3
3
  import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
4
4
  interface RunnerDeps {
@@ -1,10 +1,10 @@
1
- import type { LanguageModel, EmbeddingModel, Tool } from '../vendor.ts';
1
+ import type { LanguageModel, EmbeddingModel, Tool } from 'ai';
2
2
  export interface AgentConfig {
3
3
  id: number;
4
4
  tenant_id: string | null;
5
5
  name: string;
6
6
  description: string;
7
- type: 'chat' | 'workflow';
7
+ type: 'chat' | 'tool-use';
8
8
  model: string;
9
9
  system_prompt: string;
10
10
  owner_id: number;
@@ -0,0 +1,14 @@
1
+ import type { LanguageModel } from 'ai';
2
+ export declare function runWorkflow(opts?: {
3
+ tools?: Record<string, any>;
4
+ model?: LanguageModel;
5
+ maxSteps?: number;
6
+ }): import("ai").Tool<{
7
+ goal: string;
8
+ nodes?: any[];
9
+ }, {
10
+ result: unknown;
11
+ nodeOutputs: {
12
+ [k: string]: unknown;
13
+ };
14
+ }>;
package/dist/index.d.ts CHANGED
@@ -23,8 +23,7 @@ export { graphql } from './graphql.ts';
23
23
  export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
24
24
  export { ai } from './ai.ts';
25
25
  export type { AIHandler } from './ai.ts';
26
- export { tool, createWorkflowEngine, createSSEManager, generateWorkflow, workflow } from './workflow/index.ts';
27
- export type { Tool, Workflow, WorkflowEngine, WorkflowState, SSEEvent, WorkflowOptions, WorkflowHandler } from './workflow/index.ts';
26
+ export { runWorkflow } from './ai/workflow.ts';
28
27
  export { postgres } from './postgres/index.ts';
29
28
  export type { PostgresOptions, PostgresClient } from './postgres/types.ts';
30
29
  export { user } from './user/index.ts';