Browse Source

Task/improve web_fetch tool in fetch service (#7730)

* Improve web_fetch tool

* Update changelog
pull/7713/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
1355098bdb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 10
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  3. 70
      apps/api/src/services/fetch/fetch.service.ts
  4. 3
      libs/common/src/lib/config.ts

5
CHANGELOG.md

@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added an expiration date to the access to share the portfolio - Added an expiration date to the access to share the portfolio
- Added the date of the last usage to the access to share the portfolio - Added the date of the last usage to the access to share the portfolio
- Added support for a dedicated _OpenRouter_ engine for the `web_fetch` tool in the `FetchService`
### Changed
- Improved the logging of the `web_fetch` tool in the `FetchService`
## 3.61.0 - 2026-08-25 ## 3.61.0 - 2026-08-25

10
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -64,10 +64,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
symbol symbol
}); });
if (!trackinsightSymbol) { if (!trackinsightSymbol && symbol.includes('.')) {
trackinsightSymbol = await this.searchTrackinsightSymbol({ trackinsightSymbol = await this.searchTrackinsightSymbol({
requestTimeout, requestTimeout,
symbol: symbol.split('.')?.[0] symbol: symbol.split('.')[0]
}); });
} }
@ -213,6 +213,12 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return jsonRes['results']['docs'][0]['ticker']; return jsonRes['results']['docs'][0]['ticker'];
} }
this.logger.debug(
`Could not match a Trackinsight symbol for "${symbol}": ${JSON.stringify(
jsonRes
)}`
);
return undefined; return undefined;
}) })
.catch(({ message }) => { .catch(({ message }) => {

70
apps/api/src/services/fetch/fetch.service.ts

@ -1,7 +1,9 @@
import { redactPaths } from '@ghostfolio/api/helper/object.helper'; import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
DEFAULT_OPENROUTER_ENGINE_WEB_FETCH,
PROPERTY_API_KEY_OPENROUTER, PROPERTY_API_KEY_OPENROUTER,
PROPERTY_OPENROUTER_ENGINE_WEB_FETCH,
PROPERTY_OPENROUTER_MODEL, PROPERTY_OPENROUTER_MODEL,
PROPERTY_OPENROUTER_MODEL_WEB_FETCH, PROPERTY_OPENROUTER_MODEL_WEB_FETCH,
PROPERTY_PROXY_ROUTES, PROPERTY_PROXY_ROUTES,
@ -20,6 +22,7 @@ import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
export class FetchService implements OnModuleInit { export class FetchService implements OnModuleInit {
private readonly logger = new Logger(FetchService.name); private readonly logger = new Logger(FetchService.name);
private static readonly BODY_PREVIEW_LENGTH = 500;
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token']; private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds'); private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
@ -91,15 +94,22 @@ export class FetchService implements OnModuleInit {
url: string; url: string;
webFetchRoute: WebFetchRoute; webFetchRoute: WebFetchRoute;
}) { }) {
const [openRouterApiKey, openRouterModel, openRouterModelWebFetch] = const [
await Promise.all([ openRouterApiKey,
this.propertyService.getByKey<string>(PROPERTY_API_KEY_OPENROUTER), openRouterEngineWebFetch,
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL), openRouterModel,
this.propertyService.getByKey<string>( openRouterModelWebFetch
PROPERTY_OPENROUTER_MODEL_WEB_FETCH ] = await Promise.all([
) this.propertyService.getByKey<string>(PROPERTY_API_KEY_OPENROUTER),
]); this.propertyService.getByKey<string>(
PROPERTY_OPENROUTER_ENGINE_WEB_FETCH
),
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL),
this.propertyService.getByKey<string>(PROPERTY_OPENROUTER_MODEL_WEB_FETCH)
]);
const engine =
openRouterEngineWebFetch || DEFAULT_OPENROUTER_ENGINE_WEB_FETCH;
const model = openRouterModelWebFetch || openRouterModel; const model = openRouterModelWebFetch || openRouterModel;
if (!model || !openRouterApiKey) { if (!model || !openRouterApiKey) {
@ -119,11 +129,15 @@ export class FetchService implements OnModuleInit {
timeout: FetchService.WEB_FETCH_TIMEOUT, timeout: FetchService.WEB_FETCH_TIMEOUT,
tools: { tools: {
// Provider-executed tool: lets OpenRouter perform the actual web // Provider-executed tool: lets OpenRouter perform the actual web
// request server-side via its `web_fetch` engine. `id` and `args` // request server-side via its `web_fetch` engine. `args` and `id`
// are the OpenRouter-specific identifiers. The input schema is left // are the OpenRouter-specific identifiers. The engine must stay
// open as the arguments are supplied by the model. // nested in `parameters`, because the provider spreads `args` on the
// top level of the tool object, where OpenRouter ignores it. Nested
// keys need snake case, as only the top level gets converted. The
// input schema is left open as the arguments are supplied by the
// model.
web_fetch: tool({ web_fetch: tool({
args: { engine: 'openrouter' }, args: { parameters: { engine } },
id: 'openrouter.web_fetch', id: 'openrouter.web_fetch',
inputSchema: jsonSchema({ inputSchema: jsonSchema({
additionalProperties: true, additionalProperties: true,
@ -142,6 +156,8 @@ export class FetchService implements OnModuleInit {
text text
]; ];
let rejectedBody: string;
for (const candidate of candidates) { for (const candidate of candidates) {
if (typeof candidate !== 'string') { if (typeof candidate !== 'string') {
continue; continue;
@ -157,6 +173,8 @@ export class FetchService implements OnModuleInit {
try { try {
JSON.parse(body); JSON.parse(body);
} catch { } catch {
rejectedBody = body;
continue; continue;
} }
} }
@ -170,6 +188,16 @@ export class FetchService implements OnModuleInit {
}); });
} }
this.logger.warn(
`Web fetch tool returned no usable body for ${this.redactUrl(url)}`
);
this.logger.debug(
`Web fetch tool response for ${this.redactUrl(url)} (${
sources?.length ?? 0
} sources): ${this.getBodyPreview(rejectedBody || text)}`
);
return undefined; return undefined;
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
@ -227,6 +255,22 @@ export class FetchService implements OnModuleInit {
: requestUrl.toString(); : requestUrl.toString();
} }
/**
* Makes a short single-line preview of a response body for the log. Gives
* back a placeholder if the body is empty.
*/
private getBodyPreview(body: string): string {
if (!body) {
return '(empty)';
}
const singleLineBody = body.replace(/\s+/g, ' ').trim();
return singleLineBody.length > FetchService.BODY_PREVIEW_LENGTH
? `${singleLineBody.slice(0, FetchService.BODY_PREVIEW_LENGTH)}`
: singleLineBody;
}
private getMatchingWebFetchRoute(url: string) { private getMatchingWebFetchRoute(url: string) {
try { try {
const { hostname } = new URL(url); const { hostname } = new URL(url);

3
libs/common/src/lib/config.ts

@ -99,6 +99,7 @@ export const DEFAULT_DATE_RANGE: DateRange = 'max';
export const DEFAULT_HOST = '0.0.0.0'; export const DEFAULT_HOST = '0.0.0.0';
export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_LANGUAGE_CODE = 'en';
export const DEFAULT_LOCALE = 'en-US'; export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_OPENROUTER_ENGINE_WEB_FETCH = 'openrouter';
export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333; export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
@ -295,6 +296,8 @@ export const PROPERTY_IS_DATA_GATHERING_ENABLED = 'IS_DATA_GATHERING_ENABLED';
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED'; export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_MAX_DAILY_REQUESTS = 'MAX_DAILY_REQUESTS'; export const PROPERTY_MAX_DAILY_REQUESTS = 'MAX_DAILY_REQUESTS';
export const PROPERTY_OPENROUTER_ENGINE_WEB_FETCH =
'OPENROUTER_ENGINE_WEB_FETCH';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL'; export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH'; export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES'; export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES';

Loading…
Cancel
Save