Skip to content

Commit 4eac300

Browse files
author
Mickael Kasinski
committed
Initial commit
0 parents  commit 4eac300

17 files changed

Lines changed: 3863 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
*.js.map
4+
.env

.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
src/
2+
examples/
3+
tsconfig.json
4+
tsup.config.ts
5+
*.map

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# @qwant/answer
2+
3+
TypeScript SDK for the Qwant Answers API (`POST /v2/answer`).
4+
5+
Handles fetch, SSE parsing, partial-chunk buffering, event routing, and stream cancellation.
6+
7+
## Requirements
8+
9+
- Node.js ≥ 18
10+
- No runtime dependencies
11+
12+
## Installation
13+
14+
```bash
15+
npm install @qwant/answer
16+
```
17+
18+
## Usage
19+
20+
```ts
21+
import { AnswerClient } from '@qwant/answer';
22+
23+
const client = new AnswerClient({ apiKey: process.env.API_KEY });
24+
```
25+
26+
### Non-streaming
27+
28+
```ts
29+
const result = await client.create({
30+
query: 'Macbook néo',
31+
filter: 'frandroid.com',
32+
markdown: true,
33+
related_queries: true,
34+
});
35+
36+
console.log(result.answer);
37+
console.log(result.sources);
38+
```
39+
40+
### Streaming
41+
42+
```ts
43+
const stream = client.stream({
44+
query: 'MacBook Neo',
45+
filter: 'frandroid.com',
46+
markdown: true,
47+
style: 'editorial',
48+
language: 'fr',
49+
});
50+
51+
// Synchronous callback (useful for partial rendering)
52+
stream.onEvent((event) => {
53+
if (event.type === 'assistant') process.stdout.write(event.delta);
54+
});
55+
56+
// Async iterator (main path)
57+
for await (const event of stream) {
58+
if (event.type === 'sources') console.log('Sources:', event.sources);
59+
if (event.type === 'done') console.log('Done:', event.finish_reason);
60+
}
61+
```
62+
63+
`onEvent` and `for await` are two independent views of the same stream.
64+
65+
### Cancellation
66+
67+
```ts
68+
const stream = client.stream({ query: '...' });
69+
70+
// Via the method
71+
setTimeout(() => stream.cancel(), 2000);
72+
73+
// Via AbortSignal
74+
const ac = new AbortController();
75+
const stream = client.stream({ query: '...' }, { signal: ac.signal });
76+
77+
// Exiting the for-await loop automatically cancels the HTTP request
78+
```
79+
80+
## API
81+
82+
### `new AnswerClient(opts)`
83+
84+
| Option | Type | Default |
85+
|--------|------|---------|
86+
| `apiKey` | `string` ||
87+
| `baseURL` | `string` | `'https://api.staan.ai/v2'` |
88+
89+
### `client.create(input, signal?): Promise<AnswerV2Result>`
90+
91+
Returns the full response (non-streaming).
92+
93+
### `client.stream(input, opts?): StreamHandle`
94+
95+
Returns a `StreamHandle`:
96+
97+
| Member | Description |
98+
|--------|-------------|
99+
| `for await (const event of stream)` | Async iterator |
100+
| `stream.onEvent(handler)` | Synchronous callback, returns an unsubscribe function |
101+
| `stream.cancel()` | Cancels the HTTP request |
102+
103+
## Stream events
104+
105+
| Type | Payload |
106+
|------|---------|
107+
| `sources` | `{ sources: AnswerV2Source[] }` |
108+
| `assistant` | `{ delta: string }` |
109+
| `citation` | `{ reference_ids: number[] }` |
110+
| `usages` | `{ usages: AnswerV2UsageEntry[] }` |
111+
| `related` | `{ related_queries: string[] }` |
112+
| `done` | `{ finish_reason: string }` |
113+
114+
## Error handling
115+
116+
```ts
117+
import { AnswerApiError, AnswerNetworkError } from '@qwant/answer';
118+
119+
try {
120+
await client.create({ query: '...' });
121+
} catch (err) {
122+
if (err instanceof AnswerApiError) console.error(err.status, err.body);
123+
if (err instanceof AnswerNetworkError) console.error(err.message, err.cause);
124+
}
125+
```
126+
127+
## Development
128+
129+
```bash
130+
npm install
131+
npm run build
132+
npm run typecheck
133+
```
134+
135+
```bash
136+
API_KEY=xxx npm run example:create
137+
API_KEY=xxx npm run example:stream
138+
API_KEY=xxx npm run example:cancel
139+
```
140+
141+
## License
142+
143+
MIT

examples/basic-create.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Example: non-stream (create) usage.
3+
*
4+
* Usage:
5+
* API_KEY=xxx npm run example:create
6+
*/
7+
8+
import { AnswerClient, AnswerApiError, AnswerNetworkError } from '../src/index.js';
9+
10+
const client = new AnswerClient({
11+
apiKey: process.env['API_KEY'] ?? '',
12+
...(process.env['API_BASE_URL'] ? { baseURL: process.env['API_BASE_URL'] } : {}),
13+
});
14+
15+
try {
16+
const result = await client.create({
17+
query: 'Macbook néo',
18+
filter: 'frandroid.com',
19+
markdown: true,
20+
related_queries: true,
21+
style: 'editorial',
22+
language: 'fr',
23+
});
24+
25+
console.log('\n=== Answer ===');
26+
console.log(result.answer);
27+
console.log('\n=== Sources ===');
28+
for (const s of result.sources) {
29+
console.log(` [${s.id}] ${s.title}${s.url}`);
30+
}
31+
if (result.related_queries.length > 0) {
32+
console.log('\n=== Related queries ===');
33+
for (const q of result.related_queries) {
34+
console.log(` - ${q}`);
35+
}
36+
}
37+
if (result.usages.length > 0) {
38+
console.log('\n=== Usage ===');
39+
for (const u of result.usages) {
40+
console.log(` [${u.step}] in: ${u.input_tokens}, out: ${u.output_tokens}`);
41+
}
42+
}
43+
console.log(`\ngeneration_ms: ${result.generation_ms}`);
44+
} catch (err) {
45+
if (err instanceof AnswerApiError) {
46+
console.error(`API error ${err.status}:`, err.body);
47+
} else if (err instanceof AnswerNetworkError) {
48+
console.error('Network error:', err.message, err.cause);
49+
} else {
50+
throw err;
51+
}
52+
}

examples/stream-cancel.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Example: cancelling a stream after 2 seconds.
3+
*
4+
* Usage:
5+
* API_KEY=xxx npm run example:cancel
6+
*/
7+
8+
import { AnswerClient } from '../src/index.js';
9+
10+
const client = new AnswerClient({
11+
apiKey: process.env['API_KEY'] ?? '',
12+
...(process.env['API_BASE_URL'] ? { baseURL: process.env['API_BASE_URL'] } : {}),
13+
});
14+
15+
const stream = client.stream({
16+
query: 'Macbook néo',
17+
filter: 'frandroid.com',
18+
markdown: true,
19+
mode: 'long',
20+
});
21+
22+
// Cancel after 2 s
23+
const timeout = setTimeout(() => {
24+
console.error('\n[cancel] Aborting stream after 2s...');
25+
stream.cancel();
26+
}, 2000);
27+
28+
try {
29+
for await (const event of stream) {
30+
if (event.type === 'assistant') {
31+
process.stdout.write(event.delta);
32+
} else if (event.type === 'done') {
33+
console.error(`\n[done] ${event.finish_reason}`);
34+
}
35+
}
36+
} catch (err) {
37+
if (err instanceof Error && err.name === 'AbortError') {
38+
console.error('\n[cancelled] Stream was aborted.');
39+
} else {
40+
throw err;
41+
}
42+
} finally {
43+
clearTimeout(timeout);
44+
}

examples/stream-events.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Example: streaming with onEvent + for-await.
3+
*
4+
* Usage:
5+
* API_KEY=xxx npm run example:stream
6+
*/
7+
8+
import { AnswerClient, AnswerApiError, AnswerNetworkError } from '../src/index.js';
9+
10+
const client = new AnswerClient({
11+
apiKey: process.env['API_KEY'] ?? '',
12+
...(process.env['API_BASE_URL'] ? { baseURL: process.env['API_BASE_URL'] } : {}),
13+
});
14+
15+
const stream = client.stream({
16+
query: 'Macbook néo',
17+
filter: 'frandroid.com',
18+
markdown: true,
19+
related_queries: true,
20+
style: 'editorial',
21+
language: 'fr',
22+
});
23+
24+
// onEvent: synchronous side-channel — useful for partial rendering
25+
const unsubscribe = stream.onEvent((event) => {
26+
if (event.type === 'assistant') {
27+
process.stdout.write(event.delta);
28+
}
29+
});
30+
31+
try {
32+
for await (const event of stream) {
33+
switch (event.type) {
34+
case 'sources':
35+
console.error('\n[sources]', event.sources.map((s) => s.title).join(', '));
36+
break;
37+
case 'citation':
38+
// inline — already handled in text
39+
break;
40+
case 'related':
41+
console.error('\n[related]', event.related_queries.join(' | '));
42+
break;
43+
case 'usages':
44+
for (const u of event.usages) {
45+
console.error(`\n[usage] ${u.step} — in: ${u.input_tokens}, out: ${u.output_tokens}`);
46+
}
47+
break;
48+
case 'done':
49+
console.error(`\n[done] finish_reason: ${event.finish_reason}`);
50+
break;
51+
}
52+
}
53+
} catch (err) {
54+
if (err instanceof AnswerApiError) {
55+
console.error(`\nAPI error ${err.status}:`, err.body);
56+
} else if (err instanceof AnswerNetworkError) {
57+
console.error('\nNetwork error:', err.message);
58+
} else {
59+
throw err;
60+
}
61+
} finally {
62+
unsubscribe();
63+
}

0 commit comments

Comments
 (0)