@graphoria/server 0.2.2 → 0.2.3

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/src/cron/index.ts CHANGED
@@ -13,7 +13,7 @@ import { logger } from "../logging";
13
13
  */
14
14
  export interface ScheduledCronJob {
15
15
  name: string;
16
- job: Cron;
16
+ job: Cron<NonNullable<CronJob["context"]>>;
17
17
  config: CronJob;
18
18
  executionCount: number;
19
19
  pause: () => boolean;
@@ -106,7 +106,11 @@ const createScheduledJob = (config: CronJob, gqlQuery: GqlQueryFn<true>): Schedu
106
106
  name: config.name,
107
107
  job,
108
108
  config,
109
- executionCount,
109
+ // Getter, not a copy: the counter lives in the tick closure and callers
110
+ // (console status, getSummary) read it after ticks have happened.
111
+ get executionCount() {
112
+ return executionCount;
113
+ },
110
114
  pause: () => {
111
115
  job.pause();
112
116
  log.info("paused");
package/src/ai/README.md DELETED
@@ -1,257 +0,0 @@
1
- # ollama_helper
2
-
3
- An LLM-powered database query agent that uses tool-calling to discover a PostgreSQL schema, inspect tables, and execute queries — all from a natural language prompt. It is **cross-LLM**: the same agent runs against Ollama (default, local), OpenAI, DeepSeek, Anthropic, and any OpenAI-compatible endpoint — selected with a single env var.
4
-
5
- ## How It Works
6
-
7
- ```
8
- User prompt
9
-
10
-
11
- ┌─────────────────────────────────────────────────┐
12
- │ createAgent({ tools, systemPrompt, wrap }) │
13
- │ → (prompt) => Promise<string> │
14
- │ │
15
- │ Agent loop (ask function) │
16
- │ 1. Send systemPrompt + wrap(prompt) to LLM │
17
- │ 2. LLM responds with tool_calls? │
18
- │ ├─ Yes → strip nulls, validate via Zod, │
19
- │ │ execute tool, feed result back │
20
- │ └─ No → return final answer │
21
- │ 3. Repeat until answer or max iters │
22
- └─────────────────────────────────────────────────┘
23
-
24
-
25
- Final answer (text only — tool calls hidden)
26
- ```
27
-
28
- The agent follows a fixed 4-step discovery workflow for every query:
29
-
30
- | Step | Tool | Purpose |
31
- | ---- | ----------------- | --------------------------------------------------- |
32
- | 1 | `list_entities` | Find relevant database tables by keyword search |
33
- | 2 | `describe_entity` | Get column names, types, and relationships |
34
- | 3 | `query_data` | Execute the query using structured JSON (preferred) |
35
- | 3\* | `graphql_execute` | Raw GraphQL fallback for complex queries |
36
- | 4 | — | Present results as a Markdown table |
37
-
38
- Anti-hallucination guards prevent the model from answering without actually querying data.
39
-
40
- ## Quick Start
41
-
42
- **Prerequisites:** an LLM backend. The default is [Ollama](https://ollama.com) (local, free, no API key) running a tool-capable model. To use a hosted provider instead, set `LLM_PROVIDER` and the matching API key (see [Providers](#providers)).
43
-
44
- ```bash
45
- # Install dependencies
46
- bun install
47
-
48
- # Run with the default provider (Ollama) and default prompt
49
- bun run index.ts
50
-
51
- # Custom prompt
52
- bun run index.ts "how many students have an active subscription?"
53
-
54
- # Switch providers via env vars
55
- LLM_PROVIDER=openai LLM_MODEL=gpt-4o-mini OPENAI_API_KEY=sk-... bun run index.ts "list all events"
56
- LLM_PROVIDER=deepseek LLM_MODEL=deepseek-chat DEEPSEEK_API_KEY=sk-... bun run index.ts "list all events"
57
- LLM_PROVIDER=anthropic LLM_MODEL=claude-haiku-4-5-20251001 ANTHROPIC_API_KEY=sk-ant-... bun run index.ts "list all events"
58
- ```
59
-
60
- ### Using as a library
61
-
62
- ```typescript
63
- import { createAgent } from "ollama_helper";
64
- import { tools } from "./src/tools";
65
-
66
- // Bind domain config once — call with just a prompt
67
- const ask = createAgent({
68
- tools,
69
- systemPrompt: "You are a database assistant…",
70
- wrap: (prompt) => `Database query:\n> ${prompt}`,
71
- });
72
-
73
- const answer = await ask("list contacts grouped by role");
74
- ```
75
-
76
- ## Environment Variables
77
-
78
- | Variable | Default | Description |
79
- | ------------------- | ------------------------ | ----------------------------------------------------------- |
80
- | `LLM_PROVIDER` | `ollama` | Backend: `ollama`, `openai`, `deepseek`, or `anthropic` |
81
- | `LLM_MODEL` | per-provider | Overrides the provider's default model |
82
- | `OPENAI_API_KEY` | — | Required when `LLM_PROVIDER=openai` |
83
- | `OPENAI_BASE_URL` | — | Optional: any OpenAI-compatible endpoint (Groq, Mistral, …) |
84
- | `DEEPSEEK_API_KEY` | — | Required when `LLM_PROVIDER=deepseek` |
85
- | `ANTHROPIC_API_KEY` | — | Required when `LLM_PROVIDER=anthropic` |
86
- | `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL |
87
- | `OLLAMA_MODEL` | `gemma4:e2b` | Ollama model (alias of `LLM_MODEL`, kept for back-compat) |
88
-
89
- Defined in `.env` or inline on the command line.
90
-
91
- ## Providers
92
-
93
- The agent talks to every backend through a small `Provider` interface (`src/providers/`). Two wire formats cover everything:
94
-
95
- | Provider | `LLM_PROVIDER` | Default model | How it connects |
96
- | ---------------- | -------------- | --------------------------- | ---------------------------------- |
97
- | Ollama (default) | `ollama` | `gemma4:e2b` | Native `/api/chat`, local, no key |
98
- | OpenAI | `openai` | `gpt-4o-mini` | `openai` SDK |
99
- | DeepSeek | `deepseek` | `deepseek-chat` | `openai` SDK (OpenAI-compatible) |
100
- | Anthropic | `anthropic` | `claude-haiku-4-5-20251001` | `@anthropic-ai/sdk` (Messages API) |
101
-
102
- Because the OpenAI adapter is just a `baseURL` + key, **any OpenAI-compatible endpoint** (Groq, Mistral, Together, a local `/v1` server) works by pointing `OPENAI_BASE_URL` at it — no new code. Adding a first-class provider is one entry in the registry in `src/providers/index.ts`.
103
-
104
- The agent loop and anti-hallucination guards are provider-agnostic; each adapter only translates the normalized message/tool-call history to and from its provider's wire format.
105
-
106
- ## Recommended Ollama Models
107
-
108
- | Model | Size | Quality | Notes |
109
- | -------------- | ------ | ------- | --------------------------------------------------------------------------------------------- |
110
- | **gemma4:e2b** | 7.2 GB | ★★★★★ | **Default.** Native function-calling, built for agentic workflows. Google-recommended params. |
111
- | gemma4:e4b | 9.6 GB | ★★★★★ | Stronger reasoning, heavier. Same family, better accuracy. |
112
- | qwen3.5:2b | 2.7 GB | ★★★★☆ | Good budget option. Fast, decent tool use. |
113
- | llama3.2 | 2.0 GB | ★★☆☆☆ | Works but hallucinates more. Use `OLLAMA_MODEL=llama3.2` to test. |
114
-
115
- Each model family gets optimal sampling parameters automatically (temperature, top_p, top_k) based on vendor recommendations.
116
-
117
- ## Project Structure
118
-
119
- ```
120
- ollama_helper/
121
- ├── index.ts # Library barrel — exports ask, createAgent, AgentConfig
122
- ├── .env # Environment variables (OLLAMA_MODEL, OLLAMA_HOST)
123
- ├── package.json
124
- ├── tsconfig.json
125
- └── src/
126
- ├── types.ts # Normalized Message/ToolCall + Provider + Tool interfaces
127
- ├── providers/
128
- │ ├── index.ts # getProvider() — selects the backend from LLM_PROVIDER
129
- │ ├── ollama.ts # Native /api/chat adapter + model presets (default)
130
- │ ├── openai.ts # OpenAI-compatible adapter (OpenAI, DeepSeek, …)
131
- │ └── anthropic.ts # Anthropic Messages API adapter
132
- ├── tools.ts # 4 tool definitions (Zod schemas + mock executors)
133
- └── agent.ts # Agent loop: ask() + createAgent() factory
134
- ```
135
-
136
- ## API
137
-
138
- ### `createAgent(config: AgentConfig): (prompt: string) => Promise<string>`
139
-
140
- Pre-configure an agent with tools, system prompt, and a prompt wrapper. Returns a single-argument function — call it with any prompt string.
141
-
142
- ```typescript
143
- import { createAgent } from "ollama_helper";
144
-
145
- const ask = createAgent({
146
- tools: [...], // Tool[] — Zod-based tool definitions
147
- systemPrompt: "You are a helpful agent.", // System-level instruction to the LLM
148
- wrap: (p) => `User query:\n> ${p}`, // Wraps the raw prompt into the user message
149
- });
150
-
151
- const answer = await ask("list all contacts grouped by role");
152
- ```
153
-
154
- ### `ask(prompt, tools, systemPrompt, wrap): Promise<string>`
155
-
156
- Full-control entry point. Same as `createAgent` but without pre-binding — pass all 4 arguments per call.
157
-
158
- ```typescript
159
- import { ask } from "ollama_helper";
160
-
161
- const answer = await ask(
162
- "list contacts grouped by role",
163
- tools,
164
- "You are a database assistant.",
165
- (p) => `Query: ${p}`,
166
- );
167
- ```
168
-
169
- ### `AgentConfig`
170
-
171
- | Field | Type | Description |
172
- | -------------- | ----------------------------- | ------------------------------------------------------ |
173
- | `tools` | `Tool[]` | Zod-based tool definitions (schema + executor) |
174
- | `systemPrompt` | `string` | System-level instruction sent to the LLM |
175
- | `wrap` | `(content: string) => string` | Transforms the user prompt into the final user message |
176
-
177
- ### `Tool`
178
-
179
- ```typescript
180
- interface Tool<TSchema extends z.ZodTypeAny> {
181
- name: string; // Tool name (must match what the LLM calls)
182
- description: string; // Description sent to the LLM
183
- schema: TSchema; // Zod schema — auto-converted to JSON Schema
184
- execute: (args: z.infer<TSchema>) => Promise<unknown>; // Typed executor
185
- }
186
- ```
187
-
188
- JSON Schema is auto-generated via `z.toJSONSchema(tool.schema)`. The framework automatically strips `null` values from LLM arguments (common with small models) before Zod validation. Use the exported `jsonCoerce` helper for fields that may arrive as stringified JSON arrays/objects.
189
-
190
- ```typescript
191
- import { z } from "zod";
192
- import { jsonCoerce } from "./src/tools";
193
-
194
- const mySchema = z.object({
195
- columns: jsonCoerce(z.array(z.string())).optional(), // Handles "[\"id\"]" → ["id"]
196
- filters: jsonCoerce(z.record(z.string(), z.unknown())).optional(),
197
- });
198
- ```
199
-
200
- ## Tools
201
-
202
- The agent exposes 4 tools to the LLM. Each is defined as a `Tool` object with a Zod schema and executor:
203
-
204
- | Tool | Description |
205
- | ----------------- | -------------------------------------------------------------------------------- |
206
- | `list_entities` | Search for database tables by keyword or category |
207
- | `describe_entity` | Get column metadata, relationships, and data types for a table |
208
- | `query_data` | Execute a query using structured JSON (preferred for all list/aggregate queries) |
209
- | `graphql_execute` | Execute a raw GraphQL query (fallback for complex/nested queries) |
210
-
211
- **`query_data` input format:**
212
-
213
- ```json
214
- {
215
- "entity": "contacts",
216
- "operation": "aggregate",
217
- "groupBy": ["role"],
218
- "columns": ["id", "first_name", "last_name"],
219
- "filters": { "deleted_at": { "is_null": true } },
220
- "limit": 50
221
- }
222
- ```
223
-
224
- Filter operators: `eq`, `neq`, `like`, `ilike`, `gt`, `gte`, `lt`, `lte`, `is_null`.
225
-
226
- ## Anti-Hallucination Measures
227
-
228
- The agent employs multiple layers to prevent the LLM from fabricating data:
229
-
230
- 1. **Zod validation** — All tool arguments validated before execution; invalid args → error fed back to LLM for self-correction
231
- 2. **Null stripping** — `null` values for optional fields (common LLM mistake) automatically stripped pre-parse
232
- 3. **JSON coercion** — Stringified JSON arrays/objects auto-parsed via `jsonCoerce` helper
233
- 4. **Temperature control** — Model-specific presets (e.g., temp 0 for llama3.2 to suppress creativity)
234
- 5. **Guard clause** — If the model tries to answer without calling `query_data` or `graphql_execute`, a corrective nudge is injected and the loop retries
235
- 6. **Negative constraints** — Prompts explicitly forbid fabrication: _"NEVER fabricate, invent, or guess query results"_
236
-
237
- ## Mock Tools
238
-
239
- When a real Graphoria server is not available, the tools return hardcoded mock data mirroring the capoeira group management schema:
240
-
241
- - **5 tables:** contacts, student_profiles, classes, payments, events
242
- - **18 contacts** with roles: 12 students, 1 master, 2 guardians, 3 guests
243
- - **Full column metadata** for the contacts table (11 columns including soft-delete `deleted_at`)
244
-
245
- To replace mocks with real server calls, update the executors in `src/tools.ts` to use `fetch()` against the Graphoria REST/GraphQL endpoints.
246
-
247
- ## Design Decisions
248
-
249
- - **`createAgent` factory** — Pre-bind domain config (tools, system prompt, wrap); clean `ask(prompt)` API for callers
250
- - **`ask()` for full control** — 4 positional params when callers need per-call variations
251
- - **Zod schemas for tools** — `z.toJSONSchema()` auto-generates JSON Schema parameters; `z.infer` gives type-safe executor args; no hand-written JSON Schema
252
- - **`stripNulls` applied automatically** — LLMs pass `null` for optional fields; stripped pre-parse so callers write plain Zod schemas
253
- - **`jsonCoerce` opt-in helper** — Small LLMs stringify arrays/objects; opt-in per-field coercion
254
- - **Non-streaming only** (`stream: false`) — simpler loop logic; no need to buffer partial tool_calls across chunks
255
- - **JSON query tool over raw GraphQL** — structured JSON has far fewer syntax failure modes for LLMs than raw GraphQL string construction
256
- - **Max 10 iterations** — safety valve to prevent infinite tool-calling loops
257
- - **Bun runtime** — native fetch, zero-config TypeScript, fast startup