Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -43,7 +43,7 @@ function ContextRecommendationsPrompt({
<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.
databases/**/columns.md.
</ListItem>
<ListItem>
Source-code context: if a warehouse gap traces back to SQL, dbt, docs, or application code in{' '}
Expand Down
124 changes: 106 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,116 @@
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 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> — {description}
</ListItem>
)),
]}
</List>
</ListItem>
),
]}
</List>
</Block>
);
}

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: (
<>
an LLM-written overview of the table, data-quality caveats, and suggested uses; use for orientation, but
verify specifics against <Code>columns.md</Code> and <Code>profiling.md</Code>.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
</>
),
},
] 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
7 changes: 7 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 { extractConfiguredRepos, extractConfiguredTemplates, extractContextPresence } 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,9 @@ 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 templates = extractConfiguredTemplates(this._toolContext.projectFolder);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
const repoNames = extractConfiguredRepos(this._toolContext.projectFolder).map((repo) => repo.name);
const contextPresence = extractContextPresence(this._toolContext.projectFolder);
const skills = skillService.getSkills(this.chat.projectId);
const customCharts = this._toolContext.supportsCustomCharts
? listChartPlugins(this._toolContext.projectFolder)
Expand All @@ -612,6 +616,9 @@ class AgentManager {
skills,
customCharts,
mcpServers,
templates,
repoNames,
contextPresence,
timezone,
testMode: this.chat.testMode,
toolNames: Object.keys(this._agentTools),
Expand Down
73 changes: 73 additions & 0 deletions apps/backend/src/utils/nao-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import type { LinkedContextRepo } from '../types/context-recommendation';
import { logger } from './logger';

const ENV_PATTERN = /\$?\{\{\s*env\(['"]([^'"]+)['"]\)\s*\}\}/g;
const DATABASE_TEMPLATES = ['columns', 'preview', 'profiling', 'query_history', 'ai_summary'] as const;
const DEFAULT_DATABASE_TEMPLATES = ['columns', 'query_history', 'preview'] as const;

export type ContextPresence = {
rules: boolean;
semantics: boolean;
docs: boolean;
notionDocs: boolean;
databases: boolean;
};

export function extractRequiredEnvVars(projectFolder: string): string[] {
const configPath = path.join(projectFolder, 'nao_config.yaml');
Expand Down Expand Up @@ -62,6 +72,69 @@ export function extractConfiguredRepos(projectFolder: string): LinkedContextRepo
});
}

/** Returns the database templates enabled across all configured databases. */
export function extractConfiguredTemplates(projectFolder: string): string[] {
const configPath = path.join(projectFolder, 'nao_config.yaml');
if (!fs.existsSync(configPath)) {
return [];
}

const config = loadConfig(configPath);
if (!isRecord(config) || !Array.isArray(config.databases)) {
return [];
}

const configuredTemplates = new Set<string>();
for (const database of config.databases) {
if (!isRecord(database)) {
continue;
}

const templates = getDatabaseTemplates(database);
for (const template of templates) {
const normalizedTemplate = template === 'how_to_use' ? 'query_history' : template;
if (normalizedTemplate !== 'description') {
configuredTemplates.add(normalizedTemplate);
}
}
}

return DATABASE_TEMPLATES.filter((template) => configuredTemplates.has(template));
Comment thread
ad4mou marked this conversation as resolved.
Outdated
}

/** Returns which filesystem-backed project context is available to read. */
export function extractContextPresence(projectFolder: string): ContextPresence {
return {
rules: fs.existsSync(path.join(projectFolder, 'RULES.md')),
semantics: hasDirectoryContent(path.join(projectFolder, 'semantics')),
docs: hasDirectoryContent(path.join(projectFolder, 'docs')),
notionDocs: hasDirectoryContent(path.join(projectFolder, 'docs', 'notion')),
databases: hasDirectoryContent(path.join(projectFolder, 'databases')),
};
}

function hasDirectoryContent(directoryPath: string): boolean {
if (!fs.existsSync(directoryPath)) {
return false;
}
try {
return fs.readdirSync(directoryPath).length > 0;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
} catch {
return false;
}
}

function getDatabaseTemplates(database: Record<string, unknown>): string[] {
if (!('templates' in database) && !('accessors' in database)) {
return [...DEFAULT_DATABASE_TEMPLATES];
}

const templates = 'templates' in database ? database.templates : database.accessors;
return Array.isArray(templates)
? templates.filter((template): template is string => typeof template === 'string')
: [];
}

function loadConfig(configPath: string): unknown {
try {
return yaml.load(fs.readFileSync(configPath, 'utf-8'));
Expand Down
Loading
Loading