Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/integration-ci/integration_plan.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Plan the Codex/Pi, Go and Java integration checks, without changing Python CI.
"""Plan selected TypeScript, Go and Java integration checks, without changing Python CI.

Uses the existing Python planner's NUL-safe, rename-aware git diff reader. The
same files can land independently with each integration: absent target directories
are ignored, but an existing target may not silently lose its manifest or tests.
CI changes run every present target; TS shared dependencies run both TS targets.
CI changes run every present target; TS shared dependencies run all TS targets.
"""
import argparse
import json
Expand All @@ -18,6 +18,7 @@
TARGETS = (
('codex-sdk', 'typescript', 'typescript/packages/traceai_codex_sdk', 'package.json'),
('pi-coding-agent', 'typescript', 'typescript/packages/traceai_pi_coding_agent', 'package.json'),
('instructor', 'typescript', 'typescript/packages/traceai_instructor', 'package.json'),
('openai', 'go', 'go/openai', 'go.mod'),
('a2a', 'go', 'go/a2a', 'go.mod'),
('eino', 'go', 'go/eino', 'go.mod'),
Expand Down
4 changes: 2 additions & 2 deletions .github/integration-ci/test_integration_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def test_ci_edits_and_all_select_every_present_target(self):
self.assertEqual(self.names([path]), expected)
self.assertEqual(self.names([], all_targets=True), expected)

def test_shared_typescript_inputs_cover_both_consumers(self):
def test_shared_typescript_inputs_cover_all_consumers(self):
for path in ['typescript/pnpm-lock.yaml', 'typescript/package.json',
'typescript/pnpm-workspace.yaml', 'typescript/.npmrc',
'typescript/tsconfig.base.json', 'typescript/packages/fi-core/src/index.ts',
'typescript/packages/fi-semantic-conventions/src/index.ts']:
with self.subTest(path=path):
self.assertEqual(self.names([path]), {'codex-sdk', 'pi-coding-agent'})
self.assertEqual(self.names([path]), {name for name, language, _, _ in TARGETS if language == 'typescript'})

def test_unrelated_paths_do_not_claim_coverage(self):
self.assertEqual(self.names(['README.md', 'python/frameworks/openai/tracer.py',
Expand Down
106 changes: 69 additions & 37 deletions typescript/packages/traceai_instructor/README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,96 @@
# @traceai/instructor

OpenTelemetry instrumentation for Instructor. Part of traceAI, Future AGI's OpenTelemetry instrumentation libraries.

[Documentation](https://docs.futureagi.com/docs/integrations/traceai/instructor) · [Source](https://github.com/future-agi/traceAI/tree/dev/typescript/packages/traceai_instructor) · [Issues](https://github.com/future-agi/traceAI/issues)
Trace Instructor's TypeScript structured-output requests in Future AGI. Each extraction appears as a `CHAIN` span covering parsing and validation, with provider requests and retries beneath it when provider instrumentation is enabled.

## Install

Use Node.js 20 or later and Instructor 1.x with Zod 3.

```sh
npm install @traceai/instructor @traceai/fi-core @opentelemetry/instrumentation @opentelemetry/api@^1.9.0 @opentelemetry/context-async-hooks@^2.0.1
npm install @instructor-ai/instructor@"^1.0.0"
npm install @traceai/instructor @traceai/openai @traceai/fi-core @instructor-ai/instructor@^1.0.0 openai@^4.58.0 zod@^3.25.76 @opentelemetry/api@^1.9.0 @opentelemetry/instrumentation@^0.202.0 @opentelemetry/context-async-hooks@^2.0.1
```

The SDK ranges above match this instrumentor's module-version filters. Install
only the SDKs your application uses. For Future AGI export, set `FI_API_KEY` and
`FI_SECRET_KEY` in your environment; keep provider credentials outside source code.

## Package entry points
Set `FI_API_KEY`, `FI_SECRET_KEY`, and `OPENAI_API_KEY` in your environment. Keep credentials outside source code.

This source revision fixes the package entry points. Both CommonJS `require()`
and native Node ESM `import` use the same CommonJS implementation; this avoids
broken, extensionless ESM build imports. The corrected packages still need a
patch release. A previously published `0.1.0` tarball is not changed by this README.
## Trace an extraction

## Quick start

Load this setup **before** loading the SDK you want to instrument. This CommonJS
example registers tracing; it does not make a provider request.
Save this as `app.cjs`. Register instrumentation before loading Instructor or OpenAI.

```javascript
const { context } = require("@opentelemetry/api");
const { AsyncLocalStorageContextManager } = require("@opentelemetry/context-async-hooks");
const { register, ProjectType } = require("@traceai/fi-core");
// Standalone process only: if your OTel SDK already installs a context manager,
// reuse it instead of registering another one.
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
const { registerInstrumentations } = require("@opentelemetry/instrumentation");
const { register, ProjectType } = require("@traceai/fi-core");
const { InstructorInstrumentation } = require("@traceai/instructor");
const tracerProvider = register({
projectName: "instructor-app",
const { OpenAIInstrumentation } = require("@traceai/openai");

// In an existing OTel application, reuse its context manager and provider.
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
const provider = register({
projectName: "instructor-typescript",
projectType: ProjectType.OBSERVE,
});
const traceConfig = { hideInputs: true, hideOutputs: true };
registerInstrumentations({
tracerProvider, instrumentations: [new InstructorInstrumentation()],
tracerProvider: provider,
instrumentations: [
new InstructorInstrumentation({ traceConfig }),
new OpenAIInstrumentation({ instrumentationConfig: { enabled: false }, traceConfig }),
],
});

const createInstructor = require("@instructor-ai/instructor").default;
const OpenAI = require("openai").default;
const { z } = require("zod");
const client = createInstructor({ client: new OpenAI(), mode: "TOOLS" });
const request = {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Extract the name and age: Ada is 36." }],
response_model: {
name: "Person",
schema: z.object({ name: z.string(), age: z.number() }),
},
};

async function main() {
try {
const person = await client.chat.completions.create(request);
console.log(person.name, person.age);
} finally {
await provider.shutdown();
}
}
main().catch((error) => { console.error(error); process.exitCode = 1; });
```

Run `node app.cjs`, then open the `instructor-typescript` project in Future AGI. Look for `Instructor Extract: Person` with an OpenAI request beneath it. Keep provider instrumentation enabled to see the individual model calls made during validation retries.

The example hides prompts, parsed results, and exception details on both instrumentors. To capture content, change the corresponding `traceConfig` options after reviewing your data policy. Do not put sensitive data in schema names: names identify the extraction span.

## Stream structured output

Replace the request inside `main()` with this block. Consume the stream or close it with `return()` so its extraction span finishes. A `break` in `for await` closes it automatically.

```javascript
const stream = await client.chat.completions.create({ ...request, stream: true });
for await (const partial of stream) {
console.log(partial);
}
```

`register()` installs a tracer provider, not an async context manager. The
explicit context manager above keeps nested asynchronous calls in one trace.
Run your application's SDK calls after this setup. Call
`await tracerProvider.shutdown()` when the application finishes to flush pending
spans. For ESM, follow the documentation for your OpenTelemetry loader and SDK
instead of assuming import order alone enables automatic patching.
The span stays open through iteration, records the last parsed partial result, and reports iteration failures. Early closure sets `instructor.stream.cancelled = true` without claiming success. Instructor 1.0 opens the provider stream eagerly; newer versions can defer it until iteration. Instrumentation preserves that SDK behavior. Instructor does not support validation retries for streams.

## Span data and lifecycle

- Instructor is an extraction framework, not a model provider. The parent is a `CHAIN`, not a second `LLM` span. It does not claim a provider or duplicate provider token/cost attributes.
- Input capture contains the request messages, not the Zod schema, SDK client, or request-option headers. Output capture contains the parsed document without `_meta`.
- When Instructor supplies `_meta.usage`, numeric counts appear as `instructor.usage.prompt_tokens`, `instructor.usage.completion_tokens`, and `instructor.usage.total_tokens`. They describe supplied result metadata, not an aggregate of retries. Older SDK versions and some streaming paths do not supply usage; the instrumentor does not estimate it.
- Calls without `response_model` pass through without an Instructor span. The provider instrumentor can still trace them.
- `instrumentation.disable()` restores owned module factory patches. Existing clients stop starting extraction spans; in-flight operations still finish. After re-enabling instrumentation, create new Instructor clients.

## Compatibility and privacy
For manual setup, call `instrumentation.manuallyInstrument(instructorModule)` before constructing clients. The module object must be mutable. With native ESM, use your OpenTelemetry loader, or pass a mutable `{ default: createInstructor }` object and construct clients through that object's patched `default` property. Static import order alone does not enable automatic instrumentation.

This instruments the SDK hooks implemented by this package, not every API offered
by the provider. The install ranges above are intentionally limited to the module versions this
instrumentor patches; its peer declarations may be broader.
Traces can contain prompts, responses, and tool data; configure capture and
redaction before sending production traffic.
[Python guide](https://docs.futureagi.com/docs/integrations/traceai/instructor) · [Source](https://github.com/future-agi/traceAI/tree/dev/typescript/packages/traceai_instructor) · [Issues](https://github.com/future-agi/traceAI/issues)

## License

Expand Down
70 changes: 70 additions & 0 deletions typescript/packages/traceai_instructor/contract/readme.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Execute the actual README application against loopback-only provider and OTLP endpoints.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const http = require('node:http');
const path = require('node:path');
const vm = require('node:vm');
const { createRequire } = require('node:module');

(async () => {
const packageDir = path.resolve(__dirname, '..');
const requireFrom = createRequire(path.join(process.argv[2] || packageDir, 'package.json'));
const readme = fs.readFileSync(path.join(packageDir, 'README.md'), 'utf8');
const blocks = [...readme.matchAll(/```javascript\n([\s\S]*?)```/g)].map(match => match[1]);
const streaming = process.argv[3] === 'stream';
const code = streaming ? blocks[0].replace(
'const person = await client.chat.completions.create(request);\n console.log(person.name, person.age);', blocks[1]
) : blocks[0];
assert(code && (!streaming || code.includes('for await')));
const payloads = [];
let calls = 0;
const server = http.createServer(async (req, res) => {
const buffers = [];
for await (const chunk of req) buffers.push(chunk);
const body = JSON.parse(Buffer.concat(buffers).toString());
if (req.url === '/tracer/v1/traces') {
payloads.push(body);
res.writeHead(200, { 'content-type': 'application/json' }).end('{}');
} else if (req.url === '/v1/chat/completions') {
calls++;
const tool = { id: 'offline-tool', type: 'function', function: { name: 'Person', arguments: '{"name":"Ada","age":36}' } };
if (body.stream) {
const chunk = { id: 'offline', object: 'chat.completion.chunk', created: 0, model: body.model,
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, ...tool }] }, finish_reason: null }] };
res.writeHead(200, { 'content-type': 'text/event-stream' });
res.end(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`);
} else {
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({
id: 'offline', object: 'chat.completion', created: 0, model: body.model,
choices: [{ index: 0, finish_reason: 'tool_calls', message: { role: 'assistant', content: null, tool_calls: [tool] } }],
usage: { prompt_tokens: 4, completion_tokens: 2, total_tokens: 6 },
}));
}
} else { res.writeHead(404).end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const endpoint = `http://127.0.0.1:${server.address().port}`;
Object.assign(process.env, {
FI_API_KEY: 'offline-fi-key', FI_SECRET_KEY: 'offline-fi-secret', OPENAI_API_KEY: 'offline-provider-key',
FI_BASE_URL: endpoint, OPENAI_BASE_URL: `${endpoint}/v1`,
});
try {
await vm.runInNewContext(code, { require: requireFrom, process, console: { log() {}, error: console.error } });
assert.equal(process.exitCode || 0, 0);
assert.equal(calls, 1);
const spans = payloads.flatMap(p => p.resourceSpans.flatMap(r => r.scopeSpans.flatMap(s => s.spans)));
assert.equal(spans.length, 2, JSON.stringify(spans));
const parent = spans.find(s => s.name === 'Instructor Extract: Person');
const child = spans.find(s => s !== parent);
assert(parent && child);
assert.equal(child.parentSpanId, parent.spanId);
assert.equal(child.traceId, parent.traceId);
assert.equal(parent.attributes.find(a => a.key === 'fi.span.kind').value.stringValue, 'CHAIN');
assert.equal(child.attributes.find(a => a.key === 'fi.span.kind').value.stringValue, 'LLM');
const wire = JSON.stringify(payloads);
for (const hidden of ['Ada', 'offline-fi-key', 'offline-fi-secret', 'offline-provider-key']) assert(!wire.includes(hidden), hidden);
console.log(`README ${streaming ? 'stream' : 'request'}: one extraction + one provider span; OTLP parenting and privacy passed`);
} finally {
await new Promise(resolve => server.close(resolve));
}
})().catch(error => { console.error(error); process.exitCode = 1; });
13 changes: 10 additions & 3 deletions typescript/packages/traceai_instructor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,27 @@
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.0.1",
"@opentelemetry/instrumentation": "^0.202.0",
"@traceai/fi-core": "workspace:^",
"@traceai/fi-semantic-conventions": "workspace:^"
},
"devDependencies": {
"@opentelemetry/sdk-trace-base": "^1.30.0",
"@opentelemetry/sdk-trace-node": "^1.30.0",
"@instructor-ai/instructor": "1.7.0",
"@opentelemetry/otlp-transformer": "0.202.0",
"@opentelemetry/sdk-trace-base": "^2.0.1",
"@opentelemetry/sdk-trace-node": "^2.0.1",
"@traceai/openai": "0.1.14",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
"instructor-floor": "npm:@instructor-ai/instructor@1.0.0",
"jest": "^29.7.0",
"openai": "4.58.0",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"tsc-alias": "^1.8.10",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"zod": "3.25.76"
},
"peerDependencies": {
"@instructor-ai/instructor": "^1.0.0"
Expand Down
Loading
Loading