@yesilsci/health-ai-api 0.1.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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +129 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/sse.d.ts +15 -0
- package/dist/sse.js +69 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.js +10 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yesil Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @yesilsci/health-ai-api
|
|
2
|
+
|
|
3
|
+
Official TypeScript client for the **Yesil Health enterprise (B2B) API**.
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies — uses the platform `fetch` + `ReadableStream`
|
|
6
|
+
(Node ≥ 18, modern browsers, edge runtimes). The hard part of integrating —
|
|
7
|
+
parsing the typed Server-Sent Events stream — is handled for you.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @yesilsci/health-ai-api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { YesilHealthClient } from '@yesilsci/health-ai-api';
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
|
|
21
|
+
const client = new YesilHealthClient({
|
|
22
|
+
baseUrl: 'https://api.yesilhealth.com',
|
|
23
|
+
apiKey: process.env.YESIL_API_KEY!, // sent as X-API-Key
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
for await (const ev of client.chatStream({
|
|
27
|
+
question: 'Is metformin safe for a patient with stage 3 CKD?',
|
|
28
|
+
request_id: randomUUID(), // idempotent billing — strongly recommended
|
|
29
|
+
demographics: { age: 62, sex: 'female' },
|
|
30
|
+
health_context: 'eGFR 45 mL/min/1.73m². On lisinopril 10mg daily.',
|
|
31
|
+
})) {
|
|
32
|
+
if (ev.type === 'delta') process.stdout.write(ev.content);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
One-shot (no live rendering):
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const { text, events } = await client.chat({ question: '...' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Authentication
|
|
43
|
+
|
|
44
|
+
Every request is authenticated with your enterprise API key via the `X-API-Key`
|
|
45
|
+
header. Keep it server-side; never ship it in a browser bundle or mobile app.
|
|
46
|
+
|
|
47
|
+
## The event stream
|
|
48
|
+
|
|
49
|
+
`chatStream()` yields typed events. Route on `event.type`:
|
|
50
|
+
|
|
51
|
+
| `type` | Payload | Notes |
|
|
52
|
+
|--------------------|--------------------------------------|-------|
|
|
53
|
+
| `meta` | `phase`, `conversation_id`, research preview fields | Lifecycle + mid-stream research previews. |
|
|
54
|
+
| `delta` | `content: string` | Append `content` to build the answer text. |
|
|
55
|
+
| `citation` | citation fields | A literature citation for the answer. |
|
|
56
|
+
| `graph` | `data: {...}` | Structured chart/diagram, delivered out-of-band from text. |
|
|
57
|
+
| `memory_extracted` | `signals: MemorySignal[]` | Only if your tenant has `extract_memory` enabled. Persist and replay via `memory`. |
|
|
58
|
+
| `done` | `summary: {...}` | Terminal success event. |
|
|
59
|
+
| `error` | `message`, `code` | Terminal failure event; stream ends. |
|
|
60
|
+
|
|
61
|
+
> **Forward compatibility.** The API only ever *adds* fields and event types
|
|
62
|
+
> within a contract version — it never renames, removes, or retypes existing
|
|
63
|
+
> ones. Always include a `default` branch in your `switch` and ignore unknown
|
|
64
|
+
> event types. This client surfaces them as `UnknownEvent` rather than throwing.
|
|
65
|
+
|
|
66
|
+
## Request fields
|
|
67
|
+
|
|
68
|
+
| Field | Required | Description |
|
|
69
|
+
|------------------------|----------|-------------|
|
|
70
|
+
| `question` | ✅ | The end-user's question. |
|
|
71
|
+
| `request_id` | — | Client UUID for idempotent billing. Strongly recommended. |
|
|
72
|
+
| `demographics` | — | Small structured profile (`≤ 12 KB` JSON). |
|
|
73
|
+
| `health_context` | — | Free-form clinical context (vitals, EHR summary, wearable rollups). |
|
|
74
|
+
| `conversation_history` | — | Last turns only (max 8), `role` ∈ `user`/`assistant`. |
|
|
75
|
+
| `memory` | — | Raw memory signals to replay. |
|
|
76
|
+
| `tenant_id` | — | Optional hint; must match the key's tenant. |
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Non-2xx HTTP responses throw `YesilApiError` with `status`, `code`
|
|
81
|
+
(server `error_code`), and `message`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { YesilApiError } from '@yesilsci/health-ai-api';
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await client.chat({ question: '...' });
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (e instanceof YesilApiError) {
|
|
90
|
+
if (e.code === 'RATE_LIMITED') { /* back off */ }
|
|
91
|
+
if (e.status === 402) { /* quota exhausted / billing */ }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Common codes: `RATE_LIMITED` (429), quota/billing (402), `INVALID_API_KEY`
|
|
97
|
+
(401). In-stream failures arrive as an `error` event (see table above), not an
|
|
98
|
+
exception, unless you use the `chat()` convenience wrapper which re-throws them.
|
|
99
|
+
|
|
100
|
+
## Account endpoints
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
await client.billing(); // billing status
|
|
104
|
+
await client.quota(); // quota / rate-limit status
|
|
105
|
+
await client.usage(); // usage report
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Cancellation
|
|
109
|
+
|
|
110
|
+
Pass an `AbortSignal` to stop a stream early:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const ac = new AbortController();
|
|
114
|
+
setTimeout(() => ac.abort(), 5_000);
|
|
115
|
+
for await (const ev of client.chatStream({ question: '...' }, { signal: ac.signal })) { /* … */ }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Versioning
|
|
119
|
+
|
|
120
|
+
The client targets contract **v1** (`/api/v1`). When Yesil Health cuts a new
|
|
121
|
+
major version it ships alongside v1; bump `apiVersion` in `ClientOptions` to
|
|
122
|
+
migrate on your own schedule. v1 is never force-killed.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { BillingStatus, ChatEvent, ChatRequest, QuotaStatus, UsageReport } from './types.js';
|
|
2
|
+
export interface ClientOptions {
|
|
3
|
+
/** Base URL of the Yesil Health API, e.g. "https://api.yesilhealth.com". No trailing slash needed. */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/** Your enterprise API key. Sent as the `X-API-Key` header. */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
/** Contract version prefix. Defaults to "v1". Change only when migrating to a new major version. */
|
|
8
|
+
apiVersion?: string;
|
|
9
|
+
/** Optional custom fetch (for testing / non-standard runtimes). Defaults to global fetch. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/** Thrown for non-2xx HTTP responses. `code` mirrors the server's `error_code` when present. */
|
|
13
|
+
export declare class YesilApiError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly code?: string;
|
|
16
|
+
readonly body?: unknown;
|
|
17
|
+
constructor(message: string, status: number, code?: string, body?: unknown);
|
|
18
|
+
}
|
|
19
|
+
export interface ChatStreamOptions {
|
|
20
|
+
/** Abort the stream early. */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Client for the Yesil Health enterprise (B2B) API.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const client = new YesilHealthClient({ baseUrl: '...', apiKey: '...' });
|
|
28
|
+
* for await (const ev of client.chatStream({ question: 'Is metformin safe in CKD?' })) {
|
|
29
|
+
* if (ev.type === 'delta') process.stdout.write(ev.content);
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export declare class YesilHealthClient {
|
|
33
|
+
private readonly baseUrl;
|
|
34
|
+
private readonly apiKey;
|
|
35
|
+
private readonly prefix;
|
|
36
|
+
private readonly _fetch;
|
|
37
|
+
constructor(opts: ClientOptions);
|
|
38
|
+
/**
|
|
39
|
+
* Stream a chat answer as a sequence of typed events. The async generator
|
|
40
|
+
* yields every event verbatim; route on `event.type`. The stream ends after a
|
|
41
|
+
* `done` event (success) or an `error` event (failure).
|
|
42
|
+
*/
|
|
43
|
+
chatStream(body: ChatRequest, opts?: ChatStreamOptions): AsyncGenerator<ChatEvent, void, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Convenience wrapper: drains the stream, returns the full concatenated answer
|
|
46
|
+
* text plus the collected events. Use `chatStream` directly when you want to
|
|
47
|
+
* render tokens live.
|
|
48
|
+
*/
|
|
49
|
+
chat(body: ChatRequest, opts?: ChatStreamOptions): Promise<{
|
|
50
|
+
text: string;
|
|
51
|
+
events: ChatEvent[];
|
|
52
|
+
}>;
|
|
53
|
+
/** Current billing status for the authenticated tenant. */
|
|
54
|
+
billing(): Promise<BillingStatus>;
|
|
55
|
+
/** Current quota / rate-limit status. */
|
|
56
|
+
quota(): Promise<QuotaStatus>;
|
|
57
|
+
/** Usage report for the authenticated tenant. */
|
|
58
|
+
usage(): Promise<UsageReport>;
|
|
59
|
+
private get;
|
|
60
|
+
private toError;
|
|
61
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parseSSE } from './sse.js';
|
|
2
|
+
/** Thrown for non-2xx HTTP responses. `code` mirrors the server's `error_code` when present. */
|
|
3
|
+
export class YesilApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
body;
|
|
7
|
+
constructor(message, status, code, body) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'YesilApiError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.body = body;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Client for the Yesil Health enterprise (B2B) API.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const client = new YesilHealthClient({ baseUrl: '...', apiKey: '...' });
|
|
20
|
+
* for await (const ev of client.chatStream({ question: 'Is metformin safe in CKD?' })) {
|
|
21
|
+
* if (ev.type === 'delta') process.stdout.write(ev.content);
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export class YesilHealthClient {
|
|
25
|
+
baseUrl;
|
|
26
|
+
apiKey;
|
|
27
|
+
prefix;
|
|
28
|
+
_fetch;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
if (!opts.baseUrl)
|
|
31
|
+
throw new Error('baseUrl is required');
|
|
32
|
+
if (!opts.apiKey)
|
|
33
|
+
throw new Error('apiKey is required');
|
|
34
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
35
|
+
this.apiKey = opts.apiKey;
|
|
36
|
+
this.prefix = `/api/${opts.apiVersion ?? 'v1'}`;
|
|
37
|
+
this._fetch = opts.fetch ?? globalThis.fetch;
|
|
38
|
+
if (!this._fetch) {
|
|
39
|
+
throw new Error('No fetch available. Use Node >=18 or pass a custom fetch in ClientOptions.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stream a chat answer as a sequence of typed events. The async generator
|
|
44
|
+
* yields every event verbatim; route on `event.type`. The stream ends after a
|
|
45
|
+
* `done` event (success) or an `error` event (failure).
|
|
46
|
+
*/
|
|
47
|
+
async *chatStream(body, opts = {}) {
|
|
48
|
+
const res = await this._fetch(`${this.baseUrl}${this.prefix}/chat/stream`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'X-API-Key': this.apiKey,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
Accept: 'text/event-stream',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
signal: opts.signal,
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok || !res.body) {
|
|
59
|
+
throw await this.toError(res);
|
|
60
|
+
}
|
|
61
|
+
for await (const ev of parseSSE(res.body, opts.signal)) {
|
|
62
|
+
yield ev;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convenience wrapper: drains the stream, returns the full concatenated answer
|
|
67
|
+
* text plus the collected events. Use `chatStream` directly when you want to
|
|
68
|
+
* render tokens live.
|
|
69
|
+
*/
|
|
70
|
+
async chat(body, opts = {}) {
|
|
71
|
+
let text = '';
|
|
72
|
+
const events = [];
|
|
73
|
+
for await (const ev of this.chatStream(body, opts)) {
|
|
74
|
+
events.push(ev);
|
|
75
|
+
if (ev.type === 'delta')
|
|
76
|
+
text += ev.content;
|
|
77
|
+
if (ev.type === 'error') {
|
|
78
|
+
const err = ev;
|
|
79
|
+
throw new YesilApiError(err.message, 502, err.code, err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { text, events };
|
|
83
|
+
}
|
|
84
|
+
/** Current billing status for the authenticated tenant. */
|
|
85
|
+
billing() {
|
|
86
|
+
return this.get('/billing');
|
|
87
|
+
}
|
|
88
|
+
/** Current quota / rate-limit status. */
|
|
89
|
+
quota() {
|
|
90
|
+
return this.get('/quota');
|
|
91
|
+
}
|
|
92
|
+
/** Usage report for the authenticated tenant. */
|
|
93
|
+
usage() {
|
|
94
|
+
return this.get('/usage');
|
|
95
|
+
}
|
|
96
|
+
async get(path) {
|
|
97
|
+
const res = await this._fetch(`${this.baseUrl}${this.prefix}${path}`, {
|
|
98
|
+
method: 'GET',
|
|
99
|
+
headers: { 'X-API-Key': this.apiKey, Accept: 'application/json' },
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw await this.toError(res);
|
|
103
|
+
return (await res.json());
|
|
104
|
+
}
|
|
105
|
+
async toError(res) {
|
|
106
|
+
let body;
|
|
107
|
+
let code;
|
|
108
|
+
let message = `HTTP ${res.status}`;
|
|
109
|
+
try {
|
|
110
|
+
body = await res.json();
|
|
111
|
+
const detail = body?.detail ?? body;
|
|
112
|
+
if (detail && typeof detail === 'object') {
|
|
113
|
+
code = detail.error_code;
|
|
114
|
+
const m = detail.message;
|
|
115
|
+
if (m)
|
|
116
|
+
message = m;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
try {
|
|
121
|
+
message = (await res.text()) || message;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return new YesilApiError(message, res.status, code, body);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { YesilHealthClient, YesilApiError } from './client.js';
|
|
2
|
+
export type { ClientOptions, ChatStreamOptions } from './client.js';
|
|
3
|
+
export { parseSSE } from './sse.js';
|
|
4
|
+
export type { ChatRequest, ChatEvent, ConversationTurn, MemorySignal, MetaEvent, DeltaEvent, CitationEvent, GraphEvent, MemoryExtractedEvent, DoneEvent, ErrorEvent, UnknownEvent, BillingStatus, QuotaStatus, UsageReport, } from './types.js';
|
package/dist/index.js
ADDED
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSE parser for the Yesil Health stream.
|
|
3
|
+
*
|
|
4
|
+
* The server emits each event as:
|
|
5
|
+
* event: <type>\n
|
|
6
|
+
* data: {"type":"<type>", ...}\n
|
|
7
|
+
* \n
|
|
8
|
+
*
|
|
9
|
+
* The `type` is carried both in the `event:` line and in the JSON body. We route
|
|
10
|
+
* on the JSON body's `type` (authoritative) and fall back to the `event:` line.
|
|
11
|
+
* Events whose data line isn't valid JSON are skipped defensively rather than
|
|
12
|
+
* crashing the stream.
|
|
13
|
+
*/
|
|
14
|
+
/** Parse a raw fetch Response body (ReadableStream) into a stream of parsed JSON event objects. */
|
|
15
|
+
export declare function parseSSE(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>, void, unknown>;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSE parser for the Yesil Health stream.
|
|
3
|
+
*
|
|
4
|
+
* The server emits each event as:
|
|
5
|
+
* event: <type>\n
|
|
6
|
+
* data: {"type":"<type>", ...}\n
|
|
7
|
+
* \n
|
|
8
|
+
*
|
|
9
|
+
* The `type` is carried both in the `event:` line and in the JSON body. We route
|
|
10
|
+
* on the JSON body's `type` (authoritative) and fall back to the `event:` line.
|
|
11
|
+
* Events whose data line isn't valid JSON are skipped defensively rather than
|
|
12
|
+
* crashing the stream.
|
|
13
|
+
*/
|
|
14
|
+
/** Parse a raw fetch Response body (ReadableStream) into a stream of parsed JSON event objects. */
|
|
15
|
+
export async function* parseSSE(body, signal) {
|
|
16
|
+
const reader = body.getReader();
|
|
17
|
+
const decoder = new TextDecoder();
|
|
18
|
+
let buffer = '';
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
23
|
+
const { done, value } = await reader.read();
|
|
24
|
+
if (done)
|
|
25
|
+
break;
|
|
26
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27
|
+
// Events are separated by a blank line. Drain every complete one.
|
|
28
|
+
let sep;
|
|
29
|
+
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
30
|
+
const rawEvent = buffer.slice(0, sep);
|
|
31
|
+
buffer = buffer.slice(sep + 2);
|
|
32
|
+
const parsed = parseEventBlock(rawEvent);
|
|
33
|
+
if (parsed)
|
|
34
|
+
yield parsed;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Flush any trailing event without a final blank line.
|
|
38
|
+
const tail = parseEventBlock(buffer);
|
|
39
|
+
if (tail)
|
|
40
|
+
yield tail;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
reader.releaseLock();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function parseEventBlock(block) {
|
|
47
|
+
let eventType;
|
|
48
|
+
const dataLines = [];
|
|
49
|
+
for (const line of block.split('\n')) {
|
|
50
|
+
if (line.startsWith('event:')) {
|
|
51
|
+
eventType = line.slice(6).trim();
|
|
52
|
+
}
|
|
53
|
+
else if (line.startsWith('data:')) {
|
|
54
|
+
dataLines.push(line.slice(5).trim());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (dataLines.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const obj = JSON.parse(dataLines.join('\n'));
|
|
61
|
+
// Body `type` is authoritative; fall back to the SSE event: line.
|
|
62
|
+
if (typeof obj.type !== 'string' && eventType)
|
|
63
|
+
obj.type = eventType;
|
|
64
|
+
return obj;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null; // unparseable data line — skip, don't kill the stream
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the Yesil Health enterprise (B2B) API — contract v1.
|
|
3
|
+
*
|
|
4
|
+
* Contract rule (server side): ADD only. The server may add new fields or new
|
|
5
|
+
* SSE event types at any time; it will never rename, remove, or retype an
|
|
6
|
+
* existing one without cutting a new major version (/api/v2). This client is
|
|
7
|
+
* written to honor that: unknown event types surface as `UnknownEvent` instead
|
|
8
|
+
* of throwing, and extra fields on known events are preserved.
|
|
9
|
+
*/
|
|
10
|
+
export interface ConversationTurn {
|
|
11
|
+
role: 'user' | 'assistant';
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
/** Raw memory signal. Echoed back via the `memory_extracted` event so you can persist it. */
|
|
15
|
+
export interface MemorySignal {
|
|
16
|
+
domain?: string;
|
|
17
|
+
kind?: string;
|
|
18
|
+
flag?: string;
|
|
19
|
+
tag?: string;
|
|
20
|
+
signal_date?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface ChatRequest {
|
|
24
|
+
/** The end-user's question. Required, non-empty. */
|
|
25
|
+
question: string;
|
|
26
|
+
/** Small structured profile object. Large EHR/wearable summaries go in `health_context`. */
|
|
27
|
+
demographics?: Record<string, unknown>;
|
|
28
|
+
/** Raw memory signals. Max items enforced server-side. */
|
|
29
|
+
memory?: MemorySignal[];
|
|
30
|
+
/** Last few turns only (max 8). Role must be user|assistant. */
|
|
31
|
+
conversation_history?: ConversationTurn[];
|
|
32
|
+
/** Pre-built free-form health context block (vitals, EHR summary, wearable rollups, etc.). */
|
|
33
|
+
health_context?: string;
|
|
34
|
+
/** Client-generated UUID v1-5 for idempotent billing. Strongly recommended. */
|
|
35
|
+
request_id?: string;
|
|
36
|
+
/** Optional hint; must match the tenant resolved from the API key. */
|
|
37
|
+
tenant_id?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Stream lifecycle / phase marker. Also carries mid-stream research previews. */
|
|
40
|
+
export interface MetaEvent {
|
|
41
|
+
type: 'meta';
|
|
42
|
+
phase?: string;
|
|
43
|
+
conversation_id?: string;
|
|
44
|
+
update_type?: string;
|
|
45
|
+
subtype?: string;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
/** A chunk of the answer text. Concatenate `content` across all deltas. */
|
|
49
|
+
export interface DeltaEvent {
|
|
50
|
+
type: 'delta';
|
|
51
|
+
content: string;
|
|
52
|
+
}
|
|
53
|
+
/** A literature citation surfaced for the current answer. */
|
|
54
|
+
export interface CitationEvent {
|
|
55
|
+
type: 'citation';
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** A structured data graph (chart/diagram) payload, delivered out-of-band from text. */
|
|
59
|
+
export interface GraphEvent {
|
|
60
|
+
type: 'graph';
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Memory signals the AI extracted from this turn — only emitted if your tenant
|
|
66
|
+
* has the `extract_memory` feature flag enabled. Persist these on your side and
|
|
67
|
+
* pass them back in `memory` on future requests.
|
|
68
|
+
*/
|
|
69
|
+
export interface MemoryExtractedEvent {
|
|
70
|
+
type: 'memory_extracted';
|
|
71
|
+
signals: MemorySignal[];
|
|
72
|
+
}
|
|
73
|
+
/** Terminal event. Carries the run summary (usage, research persist payload, etc.). */
|
|
74
|
+
export interface DoneEvent {
|
|
75
|
+
type: 'done';
|
|
76
|
+
summary?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
/** Stream-level error. The stream ends after this. */
|
|
79
|
+
export interface ErrorEvent {
|
|
80
|
+
type: 'error';
|
|
81
|
+
message: string;
|
|
82
|
+
code?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Any event type this client version doesn't know about yet (forward-compatible). */
|
|
85
|
+
export interface UnknownEvent {
|
|
86
|
+
type: string;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
export type ChatEvent = MetaEvent | DeltaEvent | CitationEvent | GraphEvent | MemoryExtractedEvent | DoneEvent | ErrorEvent | UnknownEvent;
|
|
90
|
+
export interface BillingStatus {
|
|
91
|
+
[key: string]: unknown;
|
|
92
|
+
}
|
|
93
|
+
export interface QuotaStatus {
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
}
|
|
96
|
+
export interface UsageReport {
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the Yesil Health enterprise (B2B) API — contract v1.
|
|
3
|
+
*
|
|
4
|
+
* Contract rule (server side): ADD only. The server may add new fields or new
|
|
5
|
+
* SSE event types at any time; it will never rename, remove, or retype an
|
|
6
|
+
* existing one without cutting a new major version (/api/v2). This client is
|
|
7
|
+
* written to honor that: unknown event types surface as `UnknownEvent` instead
|
|
8
|
+
* of throwing, and extra fields on known events are preserved.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yesilsci/health-ai-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official TypeScript client for the Yesil Health AI enterprise (B2B) API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
19
|
+
"example": "tsx examples/basic.ts"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["yesil", "health", "ai", "sse", "medical"],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsx": "^4.7.0",
|
|
31
|
+
"typescript": "^5.4.0"
|
|
32
|
+
}
|
|
33
|
+
}
|