Skip to content

Commit 7bc3dcc

Browse files
authored
[codex] Add Jina Reader AI research source (#95)
* Add Jina Reader AI research source * Address Jina reader review feedback
1 parent c6c9fca commit 7bc3dcc

34 files changed

Lines changed: 7354 additions & 70 deletions

frontend/src/api/ai-settings.schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const tenantAiProfileSchema = z.object({
1919
keepAlive: z.string(),
2020
allowExternalResearch: z.boolean(),
2121
webResearchMode: z.string(),
22+
researchSourceKey: z.string(),
2223
includeCitations: z.boolean(),
2324
maxResearchSources: z.number(),
2425
allowedDomains: z.string(),
@@ -48,6 +49,7 @@ export const saveTenantAiProfileSchema = z.object({
4849
keepAlive: z.string(),
4950
allowExternalResearch: z.boolean(),
5051
webResearchMode: z.enum(['Disabled', 'ProviderNative', 'PatchHoundManaged', 'LocalVulnerabilityIntel']),
52+
researchSourceKey: z.string(),
5153
includeCitations: z.boolean(),
5254
maxResearchSources: z.number().int().positive(),
5355
allowedDomains: z.string(),

frontend/src/components/features/admin/GlobalEnrichmentSourceManagement.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ const nvdSource: EnrichmentSource = {
4343
displayName: 'NVD API',
4444
enabled: true,
4545
credentialMode: 'no-credential',
46+
targets: ['Scheduled'],
4647
refreshTtlHours: null,
48+
options: { jinaReader: null },
4749
credentials: {
4850
storedCredentialId: null,
4951
acceptedCredentialTypes: ['api-key'],

frontend/src/components/features/admin/GlobalEnrichmentSourceManagement.tsx

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ export function GlobalEnrichmentSourceManagement({
125125
key: source.key,
126126
displayName: source.displayName,
127127
enabled: source.enabled,
128+
targets: source.targets,
128129
refreshTtlHours: source.refreshTtlHours,
130+
options: source.options,
129131
credentials: {
130132
storedCredentialId: source.credentials.storedCredentialId ?? null,
131133
secret: source.credentials.secret,
@@ -285,6 +287,18 @@ export function GlobalEnrichmentSourceManagement({
285287
{source.key}
286288
</span>
287289
</div>
290+
{source.targets.length > 0 ? (
291+
<div className="mt-1 flex flex-wrap gap-1 pl-8">
292+
{source.targets.map((target) => (
293+
<span
294+
key={target}
295+
className="rounded-full border border-border/50 bg-background/60 px-2 py-0.5 text-[10px] text-muted-foreground"
296+
>
297+
{formatTargetLabel(target)}
298+
</span>
299+
))}
300+
</div>
301+
) : null}
288302
{source.runtime.lastError ? (
289303
<p
290304
className="mt-0.5 max-w-[200px] truncate pl-8 text-[11px] text-destructive"
@@ -537,7 +551,7 @@ function EnrichmentSourceEditorSheetContent({
537551
<div className="space-y-0.5">
538552
<p className="text-sm font-medium">Enable provider</p>
539553
<p className="text-xs text-muted-foreground">
540-
When enabled, the worker will invoke this enrichment source during vulnerability processing.
554+
When enabled, selected PatchHound processes can invoke this enrichment source.
541555
</p>
542556
</div>
543557
<input
@@ -551,6 +565,36 @@ function EnrichmentSourceEditorSheetContent({
551565
}
552566
/>
553567
</label>
568+
569+
<div className="grid gap-2 rounded-lg border border-border/60 px-4 py-3">
570+
<p className="text-sm font-medium">Process targets</p>
571+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
572+
<input
573+
type="checkbox"
574+
checked={source.targets.some((target) => target.toLowerCase() === 'scheduled')}
575+
onChange={(event) =>
576+
onUpdateSource(source.key, (current) => ({
577+
...current,
578+
targets: updateTargetList(current.targets, 'Scheduled', event.target.checked),
579+
}))
580+
}
581+
/>
582+
Scheduled enrichment worker
583+
</label>
584+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
585+
<input
586+
type="checkbox"
587+
checked={source.targets.some((target) => target.toLowerCase() === 'airesearch')}
588+
onChange={(event) =>
589+
onUpdateSource(source.key, (current) => ({
590+
...current,
591+
targets: updateTargetList(current.targets, 'AIResearch', event.target.checked),
592+
}))
593+
}
594+
/>
595+
AI research and assessments
596+
</label>
597+
</div>
554598
</FormSection>
555599

556600
<FormSection title="Configuration">
@@ -610,14 +654,139 @@ function EnrichmentSourceEditorSheetContent({
610654
</div>
611655
</FormSection>
612656

657+
{source.key === 'jina-reader' && source.options.jinaReader ? (
658+
<FormSection title="Jina Reader options">
659+
<div className="grid gap-4 sm:grid-cols-2">
660+
<div className="grid content-start gap-2">
661+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Timeout Seconds</span>
662+
<Input
663+
type="number"
664+
min={3}
665+
max={60}
666+
value={source.options.jinaReader.timeoutSeconds}
667+
onChange={(event) =>
668+
onUpdateJinaReaderOption(source, onUpdateSource, 'timeoutSeconds', Number(event.target.value || 3))
669+
}
670+
className="h-10"
671+
/>
672+
</div>
673+
<div className="grid content-start gap-2">
674+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Max Content Chars</span>
675+
<Input
676+
type="number"
677+
min={1000}
678+
max={20000}
679+
step={500}
680+
value={source.options.jinaReader.maxContentChars}
681+
onChange={(event) =>
682+
onUpdateJinaReaderOption(source, onUpdateSource, 'maxContentChars', Number(event.target.value || 1000))
683+
}
684+
className="h-10"
685+
/>
686+
</div>
687+
<div className="grid content-start gap-2">
688+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Response Format</span>
689+
<Select
690+
value={source.options.jinaReader.responseFormat}
691+
onValueChange={(value) => onUpdateJinaReaderOption(source, onUpdateSource, 'responseFormat', value ?? 'markdown')}
692+
>
693+
<SelectTrigger className="h-10 w-full rounded-lg border-border/70 bg-background px-3">
694+
<SelectValue />
695+
</SelectTrigger>
696+
<SelectContent className="rounded-xl border-border/70 bg-popover/95 backdrop-blur">
697+
<SelectItem value="markdown">Markdown</SelectItem>
698+
<SelectItem value="text">Text</SelectItem>
699+
<SelectItem value="html">HTML</SelectItem>
700+
</SelectContent>
701+
</Select>
702+
</div>
703+
<div className="grid content-start gap-2">
704+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Reader Engine</span>
705+
<label className="flex h-10 items-center gap-2 rounded-lg border border-border/70 bg-background px-3 text-sm text-muted-foreground">
706+
<input
707+
type="checkbox"
708+
checked={source.options.jinaReader.useReaderLmV2}
709+
onChange={(event) =>
710+
onUpdateJinaReaderOption(source, onUpdateSource, 'useReaderLmV2', event.target.checked)
711+
}
712+
/>
713+
Use ReaderLM v2
714+
</label>
715+
</div>
716+
<div className="grid content-start gap-2">
717+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Target Selector</span>
718+
<Input
719+
value={source.options.jinaReader.targetSelector}
720+
onChange={(event) =>
721+
onUpdateJinaReaderOption(source, onUpdateSource, 'targetSelector', event.target.value)
722+
}
723+
className="h-10"
724+
/>
725+
</div>
726+
<div className="grid content-start gap-2">
727+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Exclude Selector</span>
728+
<Input
729+
value={source.options.jinaReader.excludeSelector}
730+
onChange={(event) =>
731+
onUpdateJinaReaderOption(source, onUpdateSource, 'excludeSelector', event.target.value)
732+
}
733+
className="h-10"
734+
/>
735+
</div>
736+
<div className="grid content-start gap-2">
737+
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">Wait For Selector</span>
738+
<Input
739+
value={source.options.jinaReader.waitForSelector}
740+
onChange={(event) =>
741+
onUpdateJinaReaderOption(source, onUpdateSource, 'waitForSelector', event.target.value)
742+
}
743+
className="h-10"
744+
/>
745+
</div>
746+
<div className="grid gap-2">
747+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
748+
<input
749+
type="checkbox"
750+
checked={source.options.jinaReader.removeImages}
751+
onChange={(event) =>
752+
onUpdateJinaReaderOption(source, onUpdateSource, 'removeImages', event.target.checked)
753+
}
754+
/>
755+
Remove images
756+
</label>
757+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
758+
<input
759+
type="checkbox"
760+
checked={source.options.jinaReader.includeLinkSummary}
761+
onChange={(event) =>
762+
onUpdateJinaReaderOption(source, onUpdateSource, 'includeLinkSummary', event.target.checked)
763+
}
764+
/>
765+
Include link summary
766+
</label>
767+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
768+
<input
769+
type="checkbox"
770+
checked={source.options.jinaReader.includeImageSummary}
771+
onChange={(event) =>
772+
onUpdateJinaReaderOption(source, onUpdateSource, 'includeImageSummary', event.target.checked)
773+
}
774+
/>
775+
Include image summary
776+
</label>
777+
</div>
778+
</div>
779+
</FormSection>
780+
) : null}
781+
613782
<FormSection title="Credentials">
614783
{source.credentials.acceptedCredentialTypes.length > 0 ? (
615784
<StoredCredentialSelector
616785
source={source}
617786
storedCredentials={storedCredentials}
618787
onUpdateSource={onUpdateSource}
619788
/>
620-
) : source.credentialMode === 'global-secret' || source.key === 'nvd' ? (
789+
) : source.credentialMode === 'global-secret' || source.credentialMode === 'optional-global-secret' || source.key === 'nvd' ? (
621790
<div className="grid content-start gap-2">
622791
<span className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
623792
API Key{source.key === 'nvd' ? ' (optional)' : ''}
@@ -859,7 +1028,7 @@ function StoredCredentialSelector({
8591028
Enter a provider-specific secret below, or select a reusable global credential.
8601029
</p>
8611030
)}
862-
{source.credentialMode === 'global-secret' || source.key === 'nvd' ? (
1031+
{source.credentialMode === 'global-secret' || source.credentialMode === 'optional-global-secret' || source.key === 'nvd' ? (
8631032
<Input
8641033
type="password"
8651034
placeholder={
@@ -893,6 +1062,44 @@ function mapSourceToDraft(source: EnrichmentSource): EnrichmentSourceDraft {
8931062
}
8941063
}
8951064

1065+
function updateTargetList(targets: string[], target: string, enabled: boolean) {
1066+
const normalized = targets.filter((item) => item.toLowerCase() !== target.toLowerCase())
1067+
return enabled ? [...normalized, target] : normalized
1068+
}
1069+
1070+
function formatTargetLabel(target: string) {
1071+
switch (target.toLowerCase()) {
1072+
case 'airesearch':
1073+
return 'AI research'
1074+
case 'scheduled':
1075+
return 'Scheduled'
1076+
default:
1077+
return target
1078+
}
1079+
}
1080+
1081+
function onUpdateJinaReaderOption<TKey extends keyof NonNullable<EnrichmentSource['options']['jinaReader']>>(
1082+
source: EnrichmentSourceDraft,
1083+
onUpdateSource: (key: string, mutate: (current: EnrichmentSourceDraft) => EnrichmentSourceDraft) => void,
1084+
key: TKey,
1085+
value: NonNullable<EnrichmentSource['options']['jinaReader']>[TKey],
1086+
) {
1087+
onUpdateSource(source.key, (current) => {
1088+
if (!current.options.jinaReader) return current
1089+
1090+
return {
1091+
...current,
1092+
options: {
1093+
...current.options,
1094+
jinaReader: {
1095+
...current.options.jinaReader,
1096+
[key]: value,
1097+
},
1098+
},
1099+
}
1100+
})
1101+
}
1102+
8961103
function formatTimestamp(value: string | null) {
8971104
if (!value) return 'Never'
8981105
const date = new Date(value)
@@ -936,6 +1143,11 @@ function getProviderStatusDescription(source: EnrichmentSource) {
9361143
return 'Enriches software items with end-of-life date and support status from the endoflife.date database.'
9371144
}
9381145

1146+
if (source.key === 'jina-reader') {
1147+
if (!source.enabled) return 'Jina Reader is configured but currently inactive.'
1148+
return 'Jina Reader can fetch public web pages as research context for AI assessments. API keys are optional.'
1149+
}
1150+
9391151
return source.enabled
9401152
? 'This shared provider is available to enrich tenant vulnerability data during worker processing.'
9411153
: 'This shared provider is configured but not currently used by the worker.'

0 commit comments

Comments
 (0)