Skip to content
Open
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
32 changes: 30 additions & 2 deletions apps/backend/src/components/ai/context-recommendations-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DBContextRecommendation } from '../../db/abstractSchema';
import { Block, Bold, Code, List, ListItem, renderToMarkdown, Span, Title } from '../../lib/markdown';
import type { FlaggedContextFile } from '../../services/context-recommendations.file-costs';
import type { LinkedContextRepo } from '../../types/context-recommendation';
import type { ContextPresence } from '../../utils/nao-config';

type ExistingRecommendationSummary = Pick<
DBContextRecommendation,
Expand All @@ -16,6 +17,8 @@ type ContextRecommendationsPromptProps = {
proposeFixes?: boolean;
linkedRepos?: LinkedContextRepo[];
contextRepoConnected?: boolean;
templates?: string[];
contextPresence?: ContextPresence;
};

export function renderContextRecommendationsPrompt(props: ContextRecommendationsPromptProps): string {
Expand All @@ -30,7 +33,14 @@ function ContextRecommendationsPrompt({
proposeFixes = false,
linkedRepos = [],
contextRepoConnected = false,
templates,
contextPresence,
}: ContextRecommendationsPromptProps) {
const hasColumnsContext =
contextPresence?.databases !== false && (templates === undefined || templates.includes('columns'));
const hasProfilingContext =
contextPresence?.databases !== false && (templates === undefined || templates.includes('profiling'));

return (
<Block>
<Span>
Expand All @@ -42,8 +52,26 @@ function ContextRecommendationsPrompt({
<List ordered>
<ListItem>
Tool errors: v_messages where tool_state = &quot;output-error&quot; — cluster by the failing
table/column. Count how many tool calls failed per root cause. Cross-reference
databases/**/columns.md and description.md.
table/column. Count how many tool calls failed per root cause.
{hasColumnsContext && hasProfilingContext ? (
<>
{' '}
Cross-reference <Code>databases/**/columns.md</Code> for wrong-column failures and{' '}
<Code>databases/**/profiling.md</Code> (<Code>top_values</Code>) for wrong-value
failures.
</>
) : hasColumnsContext ? (
<>
{' '}
Cross-reference <Code>databases/**/columns.md</Code> for wrong-column failures.
</>
) : hasProfilingContext ? (
<>
{' '}
Cross-reference <Code>databases/**/profiling.md</Code> (<Code>top_values</Code>) for
wrong-value failures.
</>
) : null}
</ListItem>
<ListItem>
Source-code context: if a warehouse gap traces back to SQL, dbt, docs, or application code in{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import dbConfig, { Dialect } from '../../db/dbConfig';
import { Block, Bold, Br, Code, CodeBlock, List, ListItem, renderToMarkdown, Span, Title } from '../../lib/markdown';
import type { LinkedContextRepo } from '../../types/context-recommendation';
import { ALLOWED_APP_DB_VIEWS } from '../../utils/app-db-allowlist';
import type { ContextPresence } from '../../utils/nao-config';
import { AppDbTimestamps } from './app-db-timestamps';
import { NaoContextStructure } from './nao-context-structure';

export function renderContextRecommendationsSystemPrompt(options?: {
proposeFixes?: boolean;
linkedRepos?: LinkedContextRepo[];
templates?: string[];
contextPresence?: ContextPresence;
contextRepoConnected?: boolean;
customInstructions?: string;
}): string {
Expand All @@ -18,6 +21,8 @@ export function renderContextRecommendationsSystemPrompt(options?: {
<ContextRecommendationsSystemPrompt
proposeFixes={options?.proposeFixes ?? false}
linkedRepos={options?.linkedRepos ?? []}
templates={options?.templates}
contextPresence={options?.contextPresence}
contextRepoConnected={options?.contextRepoConnected ?? false}
customInstructions={customInstructions}
/>,
Expand All @@ -27,11 +32,15 @@ export function renderContextRecommendationsSystemPrompt(options?: {
function ContextRecommendationsSystemPrompt({
proposeFixes,
linkedRepos,
templates,
contextPresence,
contextRepoConnected,
customInstructions,
}: {
proposeFixes: boolean;
linkedRepos: LinkedContextRepo[];
templates?: string[];
contextPresence?: ContextPresence;
contextRepoConnected: boolean;
customInstructions?: string;
}) {
Expand All @@ -50,7 +59,11 @@ function ContextRecommendationsSystemPrompt({
context that you may recommend improving, correcting, or extending.
</Span>

<NaoContextStructure />
<NaoContextStructure
templates={templates}
repoNames={linkedRepos.map((repo) => repo.name)}
contextPresence={contextPresence}
Comment thread
ad4mou marked this conversation as resolved.
/>
<Span>
<Code>RULES.md</Code> and <Code>semantics/*.md</Code> hold the project-wide rules and metric definitions
the agent relies on — the most common place a fix belongs.
Expand Down
151 changes: 133 additions & 18 deletions apps/backend/src/components/ai/nao-context-structure.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,143 @@
import { Block, Italic, List, ListItem, Title } from '../../lib/markdown';
import { Block, Br, Code, List, ListItem, Title } from '../../lib/markdown';
import type { ContextPresence } from '../../utils/nao-config';

/** Explains how the nao project context is laid out on disk. Shared by every system prompt. */
export function NaoContextStructure() {
export function NaoContextStructure({
templates,
repoNames = [],
contextPresence,
}: {
templates?: string[];
repoNames?: string[];
contextPresence?: ContextPresence;
}) {
const visibleTemplates =
templates && templates.length > 0
? TEMPLATE_DESCRIPTIONS.filter(({ name }) => templates.includes(name))
: TEMPLATE_DESCRIPTIONS;
const visibleTemplateNames = new Set(visibleTemplates.map(({ name }) => name));
const repoPaths = repoNames.map((name) => `repos/${name}/`).join(', ');
const isPresent = (context: keyof ContextPresence) => contextPresence?.[context] !== false;

return (
<Block>
<Title level={2}>How nao Works</Title>
<List>
<ListItem>All the context available to you is stored as files in the project folder.</ListItem>
<ListItem>
In the <Italic>databases</Italic> folder you can find the databases context, each layer is a folder
from the databases, schema and then tables.
</ListItem>
<ListItem>
Folders are named like this: database=my_database, schema=my_schema, table=my_table.
</ListItem>
<ListItem>
Databases folders are named following this pattern: type={`<database_type>`}/database=
{`<database_name>`}/schema={`<schema_name>`}/table={`<table_name>`}.
</ListItem>
<ListItem>
Each table has files describing the table schema and the data in the table (like columns.md,
preview.md, etc.)
</ListItem>
{[
isPresent('rules') && (
<ListItem key='rules'>
<Code>RULES.md</Code> — project-wide rules; read it for project conventions.
</ListItem>
),
isPresent('semantics') && (
<ListItem key='semantics'>
<Code>semantics/</Code> — metric and business definitions; read them before calculating or
interpreting a named metric.
</ListItem>
),
isPresent('docs') && (
<ListItem key='docs'>
<Code>docs/</Code> — business documentation; read the relevant files before answering domain
questions.
{isPresent('notionDocs') && (
<>
{' '}
<Code>docs/notion/</Code> contains Notion content.
</>
)}
</ListItem>
),
repoNames.length > 0 && (
<ListItem key='repos'>
<Code>{repoPaths}</Code> — source repositories; read relevant dbt, SQL, application, or
documentation files to understand how data is produced.
</ListItem>
),
isPresent('databases') && (
<ListItem key='databases'>
<Code>databases/</Code> — warehouse context under{' '}
<Code>
type={'<database_type>'}/database={'<database_name>'}/schema={'<schema_name>'}/table=
{'<table_name>'}/
</Code>
. Inside each table folder:
<Br />
<List indent={1}>
{[
<ListItem key='annotations'>
<Code>annotations.md</Code> — human-written notes about this table; often empty,
but when it has content it is authoritative — prefer it over the generated files
if they disagree.
</ListItem>,
...visibleTemplates.map(({ name, description }) => (
<ListItem key={name}>
<Code>{name}.md</Code> —{' '}
{typeof description === 'function'
? description(visibleTemplateNames)
: description}
</ListItem>
)),
]}
</List>
</ListItem>
),
]}
</List>
</Block>
);
}

function AiSummaryDescription({ visibleTemplateNames }: { visibleTemplateNames: ReadonlySet<string> }) {
const referencedTemplates = AI_SUMMARY_REFERENCE_TEMPLATES.filter((name) => visibleTemplateNames.has(name));

return (
<>
an LLM-written overview of the table, data-quality caveats, and suggested uses; use for orientation, but do
not treat it as ground truth
{referencedTemplates.length > 0 && (
<>
{' '}
— verify specifics against{' '}
{referencedTemplates.map((name, index) => (
<>
{index > 0 && ' and '}
<Code>{name}.md</Code>
</>
))}
</>
)}
.
</>
);
}

const AI_SUMMARY_REFERENCE_TEMPLATES = ['columns', 'profiling'] as const;

const TEMPLATE_DESCRIPTIONS = [
{
name: 'columns',
description:
'table description, row count, columns with types and descriptions, plus available partitioning, clustering, or index details; read before writing SQL and never guess column names.',
},
{
name: 'preview',
description:
'a handful of sample rows showing value formats; this is a tiny, non-representative sample, so never infer null rates, volumes, or value distributions from it.',
},
{
name: 'profiling',
description:
'per-column statistics as JSONL, including null and distinct counts, min/max, and top values; read before filtering on a column value or making a data-quality claim.',
},
{
name: 'query_history',
description:
'real table usage: query count, common joins, and top queries as SQL; read to find established join keys and query patterns.',
},
{
name: 'ai_summary',
description: (visibleTemplateNames: ReadonlySet<string>) => (
<AiSummaryDescription visibleTemplateNames={visibleTemplateNames} />
),
},
] as const;
9 changes: 8 additions & 1 deletion apps/backend/src/components/ai/system-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { tokenCounter } from '../../services/token-counter';
import type { UserMemory } from '../../types/memory';
import { MEMORY_CATEGORIES, MemoryCategory } from '../../types/memory';
import { formatCurrentDate } from '../../utils/date';
import type { ContextPresence } from '../../utils/nao-config';
import { groupBy } from '../../utils/utils';
import { getDialectSqlQueryRules, getDialectToolCallRules } from './dialect-rules';
import { NaoContextStructure } from './nao-context-structure';
Expand All @@ -28,6 +29,9 @@ type SystemPromptProps = {
customCharts?: ChartPluginManifestEntry[];
/** Names of MCP servers the agent is allowed to call (tools discovered as on-disk specs). */
mcpServers?: string[];
templates?: string[];
repoNames?: string[];
contextPresence?: ContextPresence;
timezone?: string;
testMode?: boolean;
/** Names of the tools in the run's tool set — rules for surface-dependent tools (e.g. display_map) are only emitted when the tool is present. Omit to include every rule. */
Expand All @@ -51,6 +55,9 @@ export function SystemPrompt({
internalSkills = listInternalSkills(),
customCharts = [],
mcpServers = [],
templates,
repoNames = [],
contextPresence,
timezone,
testMode,
toolNames,
Expand Down Expand Up @@ -82,7 +89,7 @@ export function SystemPrompt({
<Br />
Skills can be mentioned using the / trigger.
</Span>
<NaoContextStructure />
<NaoContextStructure templates={templates} repoNames={repoNames} contextPresence={contextPresence} />
<Title level={2}>Persona</Title>
<List>
<ListItem>
Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/services/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
resolveProviderSettings,
} from '../utils/llm';
import { logger } from '../utils/logger';
import { readProjectContext } from '../utils/nao-config';
import { addPromptCache } from '../utils/prompt-cache';
import { scheduleSaveLlmInferenceRecord } from '../utils/schedule-task';
import { sanitizeTitle, TITLE_MAX_OUTPUT_TOKENS, titleFromPrompt, titleGenerationUserMessage } from '../utils/title';
Expand Down Expand Up @@ -599,6 +600,8 @@ class AgentManager {
const memories = await memoryService.safeGetUserMemories(this.chat.userId, this.chat.projectId, this.chat.id);
const userRules = getUserRules(this._toolContext.projectFolder);
const connections = getConnections(this._toolContext.projectFolder);
const { repos, templates, presence: contextPresence } = readProjectContext(this._toolContext.projectFolder);
const repoNames = repos.map((repo) => repo.name);
const skills = skillService.getSkills(this.chat.projectId);
const customCharts = this._toolContext.supportsCustomCharts
? listChartPlugins(this._toolContext.projectFolder)
Expand All @@ -612,6 +615,9 @@ class AgentManager {
skills,
customCharts,
mcpServers,
templates,
repoNames,
contextPresence,
timezone,
testMode: this.chat.testMode,
toolNames: Object.keys(this._agentTools),
Expand Down
11 changes: 9 additions & 2 deletions apps/backend/src/services/context-recommendations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as projectQueries from '../queries/project.queries';
import { DEFAULT_MAX_AUTO_PRS_PER_RUN, RecommendationInsight } from '../types/context-recommendation';
import { resolveDefaultModelSelection } from '../utils/llm';
import { logger } from '../utils/logger';
import { extractConfiguredRepos } from '../utils/nao-config';
import { readProjectContext } from '../utils/nao-config';
import { agentService } from './agent';
import { autoCreateRecommendationPullRequests, resolveRecommendationRepo } from './context-pr.service';
import { ensureFeedbackCoverage, normalizeFeedbackLinks } from './context-recommendations.feedback-coverage';
Expand Down Expand Up @@ -59,7 +59,10 @@ export async function runContextRecommendations(

try {
const project = await projectQueries.getProjectById(projectId);
const linkedRepos = project?.path ? extractConfiguredRepos(project.path) : [];
const projectContext = project?.path ? readProjectContext(project.path) : undefined;
const linkedRepos = projectContext?.repos ?? [];
const templates = projectContext?.templates;
const contextPresence = projectContext?.presence;
const contextRepo = await resolveRecommendationRepo(projectId);
const proposeFixes = !!project?.path && (!!contextRepo || linkedRepos.some((repo) => repo.repoFullName));
const fixCollector = proposeFixes
Expand All @@ -84,6 +87,8 @@ export async function runContextRecommendations(
fileReadCosts,
proposeFixes,
linkedRepos,
templates,
contextPresence,
contextRepoConnected: !!contextRepo,
}),
source: 'contextRecommendations',
Expand All @@ -103,6 +108,8 @@ export async function runContextRecommendations(
systemPrompt: renderContextRecommendationsSystemPrompt({
proposeFixes,
linkedRepos,
templates,
contextPresence,
contextRepoConnected: !!contextRepo,
customInstructions: config?.customSystemPromptInstructions ?? undefined,
}),
Expand Down
Loading
Loading