Skip to content

Commit 70ebb7c

Browse files
authored
Merge pull request #45 from TraderAlice/dev
v0.9.0-beta.3: opentypebb bug fix, npmjs publish fix
2 parents a2eccda + ccbf8da commit 70ebb7c

19 files changed

Lines changed: 1171 additions & 339 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ jobs:
115115
- name: Publish to npmjs
116116
if: steps.check.outputs.exists == 'false'
117117
working-directory: packages/opentypebb
118-
run: npm publish --registry=https://registry.npmjs.org
118+
run: |
119+
echo "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}" > .npmrc
120+
npm publish --registry=https://registry.npmjs.org
119121
env:
120122
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-alice",
3-
"version": "0.9.0-beta.2",
3+
"version": "0.9.0-beta.3",
44
"description": "File-based trading agent engine",
55
"type": "module",
66
"scripts": {

packages/opentypebb/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@traderalice/opentypebb",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "TypeScript port of OpenBB Platform — financial data infrastructure",
55
"type": "module",
66
"exports": {

packages/opentypebb/src/core/api/rest-api.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
/**
22
* REST API setup using Hono.
3-
* Maps to: openbb_core/api/rest_api.py
3+
* Maps to: openbb_core/api/rest_api.py + platform_api/main.py
44
*
55
* Creates the Hono app with:
66
* - CORS middleware
77
* - Default credential injection middleware
88
* - Error handling
99
* - Health check endpoint
10+
* - /widgets.json endpoint (for OpenBB Workspace frontend)
1011
*/
1112

1213
import { Hono } from 'hono'
1314
import { cors } from 'hono/cors'
1415
import { serve } from '@hono/node-server'
1516
import type { Credentials } from '../app/model/credentials.js'
1617

18+
const OBB_HEADERS = { 'X-Backend-Type': 'OpenBB Platform' }
19+
1720
/**
1821
* Create the Hono app with middleware configured.
1922
* Maps to: the FastAPI app creation in rest_api.py
@@ -35,6 +38,26 @@ export function createApp(
3538
return app
3639
}
3740

41+
/**
42+
* Mount the /widgets.json endpoint on the app.
43+
* Maps to: @app.get("/widgets.json") in platform_api/main.py
44+
*
45+
* The widgets config is generated once at startup and cached.
46+
* This is the endpoint that the OpenBB Workspace frontend fetches
47+
* to discover available data widgets.
48+
*
49+
* @param app - The Hono app
50+
* @param widgetsJson - Pre-built widgets configuration
51+
*/
52+
export function mountWidgetsEndpoint(
53+
app: Hono,
54+
widgetsJson: Record<string, unknown>,
55+
): void {
56+
app.get('/widgets.json', (c) => {
57+
return c.json(widgetsJson, 200, OBB_HEADERS)
58+
})
59+
}
60+
3861
/**
3962
* Start the HTTP server.
4063
* Maps to: uvicorn.run() in rest_api.py

packages/opentypebb/src/core/api/schema-registry.ts

Lines changed: 295 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/**
2+
* Widget Builder — generates widgets.json for the OpenBB Workspace frontend.
3+
*
4+
* Maps to: openbb_platform/extensions/platform_api/openbb_platform_api/utils/widgets.py
5+
*
6+
* The Python version parses the OpenAPI spec (auto-generated from Pydantic models).
7+
* In TypeScript we skip OpenAPI and directly walk:
8+
* - Router command map → routes, model names, descriptions
9+
* - Registry → which providers support each model
10+
* - Schema registry → Zod schemas for query params and data columns
11+
*/
12+
13+
import type { Router } from '../app/router.js'
14+
import type { Registry } from '../provider/registry.js'
15+
import { SCHEMA_REGISTRY } from './schema-registry.js'
16+
import { zodSchemaToWidgetParams, zodSchemaToColumnDefs } from './zod-to-widget.js'
17+
import type { WidgetParam } from './zod-to-widget.js'
18+
19+
/** Provider name display mapping (matches Python's provider_map in widgets.py). */
20+
const PROVIDER_DISPLAY: Record<string, string> = {
21+
fmp: 'FMP',
22+
yfinance: 'yFinance',
23+
fred: 'FRED',
24+
sec: 'SEC',
25+
tmx: 'TMX',
26+
ecb: 'ECB',
27+
econdb: 'EconDB',
28+
eia: 'EIA',
29+
oecd: 'OECD',
30+
finra: 'FINRA',
31+
imf: 'IMF',
32+
bls: 'BLS',
33+
cftc: 'CFTC',
34+
wsj: 'WSJ',
35+
deribit: 'Deribit',
36+
cboe: 'CBOE',
37+
multpl: 'Multpl',
38+
intrinio: 'Intrinio',
39+
federal_reserve: 'Federal Reserve',
40+
stub: 'Stub',
41+
}
42+
43+
// Strings that should always be uppercased in widget names
44+
const TO_CAPS = new Set([
45+
'pe', 'pb', 'ps', 'eps', 'ebitda', 'ebit', 'gdp', 'cpi', 'ipo',
46+
'etf', 'sec', 'fred', 'oecd', 'imf', 'ecb', 'bls', 'eia',
47+
'sp', 'ny', 'us', 'uk', 'esg', 'sloos', 'fomc', 'pce', 'nonfarm',
48+
])
49+
50+
/**
51+
* Build the widgets.json configuration from registered routes and providers.
52+
*
53+
* @param router - The root Router with all commands registered
54+
* @param registry - The provider Registry
55+
* @param apiPrefix - The API prefix (default: "/api/v1")
56+
* @returns Record of widgetId → widget configuration
57+
*/
58+
export function buildWidgetsJson(
59+
router: Router,
60+
registry: Registry,
61+
apiPrefix = '/api/v1',
62+
): Record<string, unknown> {
63+
const widgets: Record<string, unknown> = {}
64+
const commands = router.getCommandMap(apiPrefix)
65+
66+
// Build reverse index: modelName → provider names
67+
const modelToProviders = new Map<string, string[]>()
68+
for (const [providerName, provider] of registry.providers) {
69+
for (const modelName of Object.keys(provider.fetcherDict)) {
70+
const list = modelToProviders.get(modelName) ?? []
71+
list.push(providerName)
72+
modelToProviders.set(modelName, list)
73+
}
74+
}
75+
76+
for (const [routePath, cmd] of commands) {
77+
const providers = modelToProviders.get(cmd.model) ?? ['custom']
78+
79+
// Derive widget_id from route path (strip apiPrefix, convert / to _)
80+
const routeWithoutPrefix = routePath.replace(apiPrefix, '')
81+
const baseWidgetId = routeWithoutPrefix.startsWith('/')
82+
? routeWithoutPrefix.slice(1).replace(/\//g, '_')
83+
: routeWithoutPrefix.replace(/\//g, '_')
84+
85+
// Derive category and subcategory from route segments
86+
const segments = routeWithoutPrefix
87+
.split('/')
88+
.filter((s) => s.length > 0)
89+
const category = segments[0] ? toTitle(segments[0]) : ''
90+
const subCategory = segments.length > 2
91+
? toTitle(segments[1])
92+
: segments.length > 1
93+
? toTitle(segments[1])
94+
: undefined
95+
96+
// Derive widget name from route (strip category/subcategory, humanize)
97+
const name = deriveWidgetName(baseWidgetId, category, subCategory)
98+
99+
// Look up Zod schemas for this model
100+
const schemas = SCHEMA_REGISTRY[cmd.model]
101+
102+
for (const provider of providers) {
103+
const widgetId = provider === 'custom'
104+
? `${baseWidgetId}_obb`
105+
: `${baseWidgetId}_${provider}_obb`
106+
107+
// Build params from Zod query schema
108+
let params: WidgetParam[] = []
109+
if (schemas) {
110+
params = zodSchemaToWidgetParams(schemas.queryParams)
111+
}
112+
113+
// Add hidden provider param (matches Python behavior)
114+
if (provider !== 'custom') {
115+
params.push({
116+
paramName: 'provider',
117+
label: 'Provider',
118+
description: 'Data source provider.',
119+
type: 'text',
120+
value: provider,
121+
optional: false,
122+
show: false,
123+
})
124+
}
125+
126+
// Build column definitions from Zod data schema
127+
let columnsDefs: unknown[] = []
128+
if (schemas) {
129+
columnsDefs = zodSchemaToColumnDefs(schemas.data)
130+
}
131+
132+
const providerDisplayName = PROVIDER_DISPLAY[provider] ?? toTitle(provider)
133+
134+
const widgetConfig: Record<string, unknown> = {
135+
name,
136+
description: cmd.description,
137+
category: category.replace('Fixedincome', 'Fixed Income'),
138+
type: 'table',
139+
searchCategory: category.replace('Fixedincome', 'Fixed Income'),
140+
widgetId,
141+
mcp_tool: {
142+
mcp_server: 'Open Data Platform',
143+
tool_id: baseWidgetId,
144+
},
145+
params,
146+
endpoint: routePath,
147+
runButton: false,
148+
gridData: { w: 40, h: 15 },
149+
data: {
150+
dataKey: 'results',
151+
table: {
152+
showAll: true,
153+
enableAdvanced: true,
154+
...(columnsDefs.length > 0 ? { columnsDefs } : {}),
155+
},
156+
},
157+
source: [providerDisplayName],
158+
}
159+
160+
if (subCategory && segments.length > 2) {
161+
widgetConfig.subCategory = subCategory
162+
}
163+
164+
widgets[widgetId] = widgetConfig
165+
}
166+
}
167+
168+
return widgets
169+
}
170+
171+
/** Convert a snake_case segment to Title Case, uppercasing known acronyms. */
172+
function toTitle(s: string): string {
173+
return s
174+
.replace(/_/g, ' ')
175+
.split(' ')
176+
.map((w) => {
177+
const lower = w.toLowerCase()
178+
if (TO_CAPS.has(lower)) return lower.toUpperCase()
179+
return lower.charAt(0).toUpperCase() + lower.slice(1)
180+
})
181+
.join(' ')
182+
}
183+
184+
/** Derive a human-readable widget name from the base widget ID. */
185+
function deriveWidgetName(widgetId: string, category: string, subCategory?: string): string {
186+
let name = widgetId
187+
.replace(/_/g, ' ')
188+
.split(' ')
189+
.map((w) => {
190+
const lower = w.toLowerCase()
191+
if (TO_CAPS.has(lower)) return lower.toUpperCase()
192+
return lower.charAt(0).toUpperCase() + lower.slice(1)
193+
})
194+
.join(' ')
195+
196+
// Remove category and subcategory from name to avoid duplication
197+
if (category) {
198+
name = name.replace(new RegExp(`^${escapeRegex(category)}\\s*`, 'i'), '')
199+
}
200+
if (subCategory) {
201+
name = name.replace(new RegExp(`^${escapeRegex(subCategory)}\\s*`, 'i'), '')
202+
}
203+
204+
return name.trim() || widgetId
205+
}
206+
207+
function escapeRegex(s: string): string {
208+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
209+
}

0 commit comments

Comments
 (0)