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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const RESEARCH_QUERY_CONCURRENCY = 3;
const RESEARCH_RESULTS_PER_QUERY = 6;
const RESEARCH_RESULT_TEXT_CHARACTERS = 2_500;
const RESEARCH_SCRAPE_CHARACTERS = 12_000;
const RESEARCH_PROVIDER_OPTIONS = {
anthropic: { structuredOutputMode: "outputFormat" as const },
};

interface ResearchEvidenceSource {
content: string;
Expand Down Expand Up @@ -143,6 +146,7 @@ function createQueryStep(id: string, config: ResearchWorkflowPrompts) {
const response = await agent.generate(researchPassPrompt(config, inputData.query, evidence), {
activeTools: [],
abortSignal,
providerOptions: RESEARCH_PROVIDER_OPTIONS,
requestContext: research.requestContext,
structuredOutput: { schema: ResearchPassDraftSchema },
});
Expand All @@ -167,6 +171,7 @@ function createSynthesisStep(id: string, config: ResearchWorkflowPrompts) {
const response = await agent.generate(config.synthesisPrompt(inputData), {
activeTools: [],
abortSignal,
providerOptions: RESEARCH_PROVIDER_OPTIONS,
requestContext,
structuredOutput: { schema: ResearchSynthesisDraftSchema },
});
Expand Down Expand Up @@ -283,6 +288,7 @@ function researchPassPrompt(
return [
config.queryPrompt(query),
"Use only the provider evidence below. For Exa citations, copy providerResultId and URL exactly. For Firecrawl citations, copy the URL exactly.",
"Set providerResultId to an empty string for every Firecrawl citation.",
"Do not cite sourceId directly and do not add sources that are absent from this evidence pack.",
"",
JSON.stringify(evidence, null, 2),
Expand Down
29 changes: 27 additions & 2 deletions packages/agent-core/src/mastra/workflows/research-provenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ const SourceReferenceSchema = z.discriminatedUnion("provider", [
}),
]);

const SourceReferenceDraftSchema = z.strictObject({
provider: z.enum(["exa", "firecrawl"]),
providerResultId: z.string().trim().max(500),
url: z.string().trim().min(1),
});

export const ResearchPassDraftSchema = z.strictObject({
claims: z.array(
z.strictObject({
claim: z.string().trim().min(1),
sources: z.array(SourceReferenceSchema).min(1),
sources: z.array(SourceReferenceDraftSchema).min(1),
}),
),
summary: z.string().trim().min(1),
Expand All @@ -45,6 +51,7 @@ export const ResearchSynthesisDraftSchema = z.strictObject({
});

type SourceReference = z.infer<typeof SourceReferenceSchema>;
type SourceReferenceDraft = z.infer<typeof SourceReferenceDraftSchema>;
type ResearchPassDraft = z.infer<typeof ResearchPassDraftSchema>;

interface EvidenceCollector {
Expand Down Expand Up @@ -81,7 +88,11 @@ export function validateResearchPass(
const citedSources = new Map<string, ResearchSource>();
const claims = draft.claims.map((claim) => ({
claim: claim.claim,
sourceIds: resolveClaimSources(claim.sources, collector, citedSources),
sourceIds: resolveClaimSources(
claim.sources.map(sourceReferenceFromDraft),
collector,
citedSources,
),
}));
return ResearchFindingSchema.parse({
claims,
Expand All @@ -91,6 +102,20 @@ export function validateResearchPass(
});
}

function sourceReferenceFromDraft(draft: SourceReferenceDraft): SourceReference {
if (draft.provider === "exa") {
return SourceReferenceSchema.parse({
provider: draft.provider,
providerResultId: draft.providerResultId,
url: draft.url,
});
}
if (draft.providerResultId.length > 0) {
throw new Error("A Firecrawl citation cannot include an Exa result identifier.");
}
return SourceReferenceSchema.parse({ provider: draft.provider, url: draft.url });
}

export function validateSynthesisClaims(
claims: ResearchClaim[],
sources: ResearchSource[],
Expand Down