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
6 changes: 3 additions & 3 deletions test-apps/stock-watchlist-agent-js/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# OpenAI
OPENAI_API_KEY=sk-...
# Optional model overrides
# OPENAI_MODEL=gpt-4o
# OPENAI_SEARCH_MODEL=gpt-4o-mini
# OPENAI_EVAL_MODEL=gpt-4o-mini
# OPENAI_MODEL=gpt-5.4-nano
# OPENAI_SEARCH_MODEL=gpt-5.4-nano
# OPENAI_EVAL_MODEL=gpt-5.4-nano

# Datadog LLM Observability
DD_API_KEY=<your-datadog-api-key>
Expand Down
26 changes: 25 additions & 1 deletion test-apps/stock-watchlist-agent-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ JavaScript translation of `test-apps/stock-watchlist-agent`, built with OpenAI's

```
llmobs.trace(kind="agent", name="analyze_portfolio") ← evals attach here
├── resolve_tickers_from_images (workflow) ← only when image inputs are given
│ └── identify_ticker (llm, one per image) → OpenAI vision call, image_parts annotated
└── orchestrator (OpenAI Responses ReAct loop)
├── delegate_research (tool, batched tickers)
│ └── stock_researcher (OpenAI Responses ReAct loop)
Expand All @@ -17,6 +19,8 @@ llmobs.trace(kind="agent", name="analyze_portfolio") ← evals attach her
└── stock_researcher ...
```

Inputs can be ticker symbols, images, or a mix of both. When images are provided, a vision step runs first inside the root `analyze_portfolio` span: one LLM call per image identifies the public company shown and returns its ticker symbol, and the resolved tickers are merged with any tickers passed directly. Images that cannot be matched to a public company come back as `UNKNOWN` and are skipped. Image resolution and research therefore share a single trace.

The orchestrator plans how to batch tickers by sector/theme, delegates batches to researcher agents, and synthesizes a portfolio briefing. Each researcher runs a multi-step ReAct loop with four research tools backed by OpenAI web search. Post-run evaluations (completeness, sentiment consistency, factual grounding) are submitted to LLM Observability.

## Development
Expand All @@ -42,9 +46,27 @@ npm install /path/to/dd-trace-js/packages/dd-trace

```bash
# Either export OPENAI_API_KEY or set it in .env

# a) All three inputs are ticker symbols
npm start -- AAPL GOOGL NVDA

# b) First two inputs are images, third is a ticker symbol
npm start -- logos/apple.png logos/google.png NVDA

# --image forces an argument to be read as an image
npm start -- AAPL --image logos/nvidia.png
```

The `logos/` directory holds small sample wordmark images for Apple, Google, and NVIDIA so the image path can be run without supplying your own files.

Arguments ending in `.png`, `.jpg`, `.jpeg`, `.gif`, or `.webp` are treated as local image files automatically. Use `--image <path>` to force an argument to be read as an image. Each image is read as base64 so the same bytes are sent to OpenAI and attached to the trace. Image-to-ticker translation uses `OPENAI_MODEL` (defaults to `gpt-5.4-nano`), so that model must support image inputs.

### Images on spans

`identify_ticker` is annotated as an `llm`-kind span whose user message carries `imageParts: [{ mimeType, content }]`, which the SDK emits as `image_parts: [{ mime_type, content }]`.

To see an image input in Datadog, open the trace in **LLM Observability > Traces** and select the span named **`identify_ticker`**.

## Running with Datadog LLM Observability

```bash
Expand Down Expand Up @@ -102,6 +124,7 @@ When LLMObs is enabled, three evaluations run after each analysis and are submit
## Project Structure

```
logos/ # Small sample images for the image-input path
src/
├── main.js # CLI entry point, eval runner
├── observability.js # .env loading, dd-trace-js initialization, LLMObs helpers
Expand All @@ -111,5 +134,6 @@ src/
├── orchestrator.js # ReAct orchestrator, delegation tool, agent span
├── researcher.js # Per-batch research agent with 4 tools
├── responses-agent.js # Generic Responses API function-calling loop
└── searcher.js # OpenAI Responses API web search helper
├── searcher.js # OpenAI Responses API web search helper
└── vision.js # Image → ticker symbol translation
```
Binary file added test-apps/stock-watchlist-agent-js/logos/apple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 31 additions & 32 deletions test-apps/stock-watchlist-agent-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test-apps/stock-watchlist-agent-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"node": ">=20"
},
"dependencies": {
"dd-trace": "latest",
"dd-trace": "^6.10.0",
"dotenv": "^16.4.7",
"openai": "^4.104.0"
}
Expand Down
24 changes: 20 additions & 4 deletions test-apps/stock-watchlist-agent-js/src/agents/orchestrator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { researchStocks } = require('./researcher')
const { resolveTickersFromImages } = require('./vision')
const { runResponsesAgent } = require('./responses-agent')
const { portfolioBriefingSchema, validatePortfolioBriefing } = require('../models')
const { annotate, exportSpan, traceSpan } = require('../observability')
Expand Down Expand Up @@ -106,13 +107,28 @@ function formatUtcNow () {
return `${yyyy}-${mm}-${dd} ${hh}:${min} UTC`
}

async function analyzePortfolio (tickers) {
async function analyzePortfolio (inputTickers, { images = [], onImagesResolved } = {}) {
return traceSpan({ kind: 'agent', name: 'analyze_portfolio' }, async span => {
const now = formatUtcNow()
const prompt = `Analyze these stock tickers: ${tickers.join(', ')}. Current time: ${now}`
const spanContext = exportSpan(span)

annotate(span, { inputData: tickers, metadata: { generated_at: now } })
let tickers = inputTickers
let identifications = []
if (images.length > 0) {
const resolved = await resolveTickersFromImages(images)
identifications = resolved.identifications
tickers = [...new Set([...inputTickers, ...resolved.tickers])]
if (onImagesResolved) onImagesResolved(identifications)
if (tickers.length === 0) {
throw new Error('No ticker symbols could be identified from the provided image(s)')
}
}

const prompt = `Analyze these stock tickers: ${tickers.join(', ')}. Current time: ${now}`
annotate(span, {
inputData: images.length > 0 ? { tickers: inputTickers, images } : inputTickers,
metadata: { generated_at: now, resolved_tickers: tickers },
})
const briefing = await traceSpan({ kind: 'agent', name: 'orchestrator' }, async orchestratorSpan => {
annotate(orchestratorSpan, { inputData: prompt, metadata: { generated_at: now } })
const result = await runResponsesAgent({
Expand All @@ -128,7 +144,7 @@ async function analyzePortfolio (tickers) {
return validated
})
annotate(span, { outputData: briefing })
return { briefing, spanContext }
return { briefing, spanContext, tickers, identifications }
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { annotate, traceSpan } = require('../observability')
const OpenAI = require('openai')

const client = new OpenAI()
const DEFAULT_MODEL = process.env.OPENAI_MODEL || 'gpt-4o'
const DEFAULT_MODEL = process.env.OPENAI_MODEL || 'gpt-5.4-nano'

function jsonSchemaFormat (name, schema) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const SEARCH_INSTRUCTIONS = [

async function search (query) {
const response = await client.responses.create({
model: process.env.OPENAI_SEARCH_MODEL || 'gpt-4o-mini',
model: process.env.OPENAI_SEARCH_MODEL || 'gpt-5.4-nano',
instructions: SEARCH_INSTRUCTIONS,
input: query,
tools: [{ type: 'web_search' }],
Expand Down
Loading