mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor: consolidate Claude SDK interactions into provider architecture
Add executeSimpleQuery() and executeStreamingQuery() methods to ClaudeProvider to consolidate duplicated SDK interaction patterns across route handlers. Changes: - Add executeSimpleQuery() for one-shot queries (title gen, descriptions, etc.) - Add executeStreamingQuery() for queries with tools and structured output - Add extractTextFromStream() private method (was duplicated 5+ times) - Add createPromptGenerator() for multi-part prompts (images, text) - Add new types: SimpleQueryOptions, SimpleQueryResult, StreamingQueryOptions, StreamingQueryResult, PromptContentBlock - Update BaseProvider with abstract method signatures Refactored routes to use provider: - generate-title.ts: uses executeSimpleQuery() - describe-file.ts: uses executeSimpleQuery() - describe-image.ts: uses executeSimpleQuery() - enhance.ts: uses executeSimpleQuery() - generate-spec.ts: uses executeStreamingQuery() - generate-features-from-spec.ts: uses executeStreamingQuery() - generate-suggestions.ts: uses executeStreamingQuery() Benefits: - Eliminates 5+ duplicated extractTextFromStream() functions - All SDK interactions now go through provider architecture - Consistent error handling and logging - Support for streaming callbacks (onText, onToolUse) - Support for structured output (outputFormat) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,10 @@ import type {
|
|||||||
InstallationStatus,
|
InstallationStatus,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
|
SimpleQueryOptions,
|
||||||
|
SimpleQueryResult,
|
||||||
|
StreamingQueryOptions,
|
||||||
|
StreamingQueryResult,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +39,22 @@ export abstract class BaseProvider {
|
|||||||
*/
|
*/
|
||||||
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
|
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a simple one-shot query and return text directly
|
||||||
|
* Use for quick completions without tools (title gen, descriptions, etc.)
|
||||||
|
* @param options Simple query options
|
||||||
|
* @returns Query result with text
|
||||||
|
*/
|
||||||
|
abstract executeSimpleQuery(options: SimpleQueryOptions): Promise<SimpleQueryResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a streaming query with tools and/or structured output
|
||||||
|
* Use for queries that need tools, progress callbacks, or structured JSON output
|
||||||
|
* @param options Streaming query options
|
||||||
|
* @returns Query result with text and optional structured output
|
||||||
|
*/
|
||||||
|
abstract executeStreamingQuery(options: StreamingQueryOptions): Promise<StreamingQueryResult>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if the provider is installed and configured
|
* Detect if the provider is installed and configured
|
||||||
* @returns Installation status
|
* @returns Installation status
|
||||||
|
|||||||
@@ -3,15 +3,25 @@
|
|||||||
*
|
*
|
||||||
* Wraps the @anthropic-ai/claude-agent-sdk for seamless integration
|
* Wraps the @anthropic-ai/claude-agent-sdk for seamless integration
|
||||||
* with the provider architecture.
|
* with the provider architecture.
|
||||||
|
*
|
||||||
|
* Provides two query methods:
|
||||||
|
* - executeQuery(): Streaming async generator for complex multi-turn sessions
|
||||||
|
* - executeSimpleQuery(): One-shot queries that return text directly (title gen, descriptions, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { BaseProvider } from './base-provider.js';
|
import { BaseProvider } from './base-provider.js';
|
||||||
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
|
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||||
import type {
|
import type {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
ProviderMessage,
|
ProviderMessage,
|
||||||
InstallationStatus,
|
InstallationStatus,
|
||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
|
SimpleQueryOptions,
|
||||||
|
SimpleQueryResult,
|
||||||
|
StreamingQueryOptions,
|
||||||
|
StreamingQueryResult,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
export class ClaudeProvider extends BaseProvider {
|
export class ClaudeProvider extends BaseProvider {
|
||||||
@@ -175,4 +185,225 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
|
const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
|
||||||
return supportedFeatures.includes(feature);
|
return supportedFeatures.includes(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a simple one-shot query and return text directly
|
||||||
|
*
|
||||||
|
* Use this for:
|
||||||
|
* - Title generation from description
|
||||||
|
* - Text enhancement
|
||||||
|
* - File/image description
|
||||||
|
* - Any quick, single-turn completion without tools
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
|
* const result = await provider.executeSimpleQuery({
|
||||||
|
* prompt: 'Generate a title for: User authentication feature',
|
||||||
|
* systemPrompt: 'You are a title generator...',
|
||||||
|
* });
|
||||||
|
* if (result.success) console.log(result.text);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async executeSimpleQuery(options: SimpleQueryOptions): Promise<SimpleQueryResult> {
|
||||||
|
const { prompt, model, systemPrompt, abortController } = options;
|
||||||
|
|
||||||
|
const resolvedModel = resolveModelString(model, CLAUDE_MODEL_MAP.haiku);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sdkOptions: Options = {
|
||||||
|
model: resolvedModel,
|
||||||
|
systemPrompt,
|
||||||
|
maxTurns: 1,
|
||||||
|
allowedTools: [],
|
||||||
|
permissionMode: 'acceptEdits',
|
||||||
|
abortController,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle both string prompts and multi-part content blocks
|
||||||
|
const stream = Array.isArray(prompt)
|
||||||
|
? query({ prompt: this.createPromptGenerator(prompt), options: sdkOptions })
|
||||||
|
: query({ prompt, options: sdkOptions });
|
||||||
|
const { text } = await this.extractTextFromStream(stream);
|
||||||
|
|
||||||
|
if (!text || text.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
success: false,
|
||||||
|
error: 'Empty response from Claude',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: text.trim(),
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('[ClaudeProvider] executeSimpleQuery() error:', errorMessage);
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a streaming query with tools and/or structured output
|
||||||
|
*
|
||||||
|
* Use this for:
|
||||||
|
* - Spec generation (with JSON schema output)
|
||||||
|
* - Feature generation from specs
|
||||||
|
* - Suggestions generation
|
||||||
|
* - Any query that needs tools or progress callbacks
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const provider = ProviderFactory.getProviderForModel('opus');
|
||||||
|
* const result = await provider.executeStreamingQuery({
|
||||||
|
* prompt: 'Analyze this project...',
|
||||||
|
* cwd: '/path/to/project',
|
||||||
|
* allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
|
* outputFormat: { type: 'json_schema', schema: mySchema },
|
||||||
|
* onText: (chunk) => console.log('Progress:', chunk),
|
||||||
|
* });
|
||||||
|
* console.log(result.structuredOutput);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async executeStreamingQuery(options: StreamingQueryOptions): Promise<StreamingQueryResult> {
|
||||||
|
const {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
systemPrompt,
|
||||||
|
cwd,
|
||||||
|
maxTurns = 100,
|
||||||
|
allowedTools = ['Read', 'Glob', 'Grep'],
|
||||||
|
abortController,
|
||||||
|
outputFormat,
|
||||||
|
onText,
|
||||||
|
onToolUse,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const resolvedModel = resolveModelString(model, CLAUDE_MODEL_MAP.haiku);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sdkOptions: Options = {
|
||||||
|
model: resolvedModel,
|
||||||
|
systemPrompt,
|
||||||
|
maxTurns,
|
||||||
|
cwd,
|
||||||
|
allowedTools: [...allowedTools],
|
||||||
|
permissionMode: 'acceptEdits',
|
||||||
|
abortController,
|
||||||
|
...(outputFormat && { outputFormat }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle both string prompts and multi-part content blocks
|
||||||
|
const stream = Array.isArray(prompt)
|
||||||
|
? query({ prompt: this.createPromptGenerator(prompt), options: sdkOptions })
|
||||||
|
: query({ prompt, options: sdkOptions });
|
||||||
|
const { text, structuredOutput } = await this.extractTextFromStream(stream, {
|
||||||
|
onText,
|
||||||
|
onToolUse,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!text && !structuredOutput) {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
success: false,
|
||||||
|
error: 'Empty response from Claude',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: text.trim(),
|
||||||
|
success: true,
|
||||||
|
structuredOutput,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('[ClaudeProvider] executeStreamingQuery() error:', errorMessage);
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a multi-part prompt generator for content blocks
|
||||||
|
*/
|
||||||
|
private createPromptGenerator(content: Array<{ type: string; text?: string; source?: object }>) {
|
||||||
|
// Return an async generator that yields SDK user messages
|
||||||
|
// The SDK expects this format for multi-part prompts
|
||||||
|
return (async function* () {
|
||||||
|
yield {
|
||||||
|
type: 'user' as const,
|
||||||
|
session_id: '',
|
||||||
|
message: { role: 'user' as const, content },
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text and structured output from SDK stream
|
||||||
|
*
|
||||||
|
* This consolidates the duplicated extractTextFromStream() function
|
||||||
|
* that was copied across 5+ route files.
|
||||||
|
*/
|
||||||
|
private async extractTextFromStream(
|
||||||
|
stream: AsyncIterable<unknown>,
|
||||||
|
handlers?: {
|
||||||
|
onText?: (text: string) => void;
|
||||||
|
onToolUse?: (name: string, input: unknown) => void;
|
||||||
|
}
|
||||||
|
): Promise<{ text: string; structuredOutput?: unknown }> {
|
||||||
|
let responseText = '';
|
||||||
|
let structuredOutput: unknown = undefined;
|
||||||
|
|
||||||
|
for await (const msg of stream) {
|
||||||
|
const message = msg as {
|
||||||
|
type: string;
|
||||||
|
subtype?: string;
|
||||||
|
result?: string;
|
||||||
|
structured_output?: unknown;
|
||||||
|
message?: {
|
||||||
|
content?: Array<{ type: string; text?: string; name?: string; input?: unknown }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message.type === 'assistant' && message.message?.content) {
|
||||||
|
for (const block of message.message.content) {
|
||||||
|
if (block.type === 'text' && block.text) {
|
||||||
|
responseText += block.text;
|
||||||
|
handlers?.onText?.(block.text);
|
||||||
|
} else if (block.type === 'tool_use' && block.name) {
|
||||||
|
handlers?.onToolUse?.(block.name, block.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message.type === 'result' && message.subtype === 'success') {
|
||||||
|
if (message.result) {
|
||||||
|
responseText = message.result;
|
||||||
|
}
|
||||||
|
if (message.structured_output) {
|
||||||
|
structuredOutput = message.structured_output;
|
||||||
|
}
|
||||||
|
} else if (message.type === 'result' && message.subtype === 'error_max_turns') {
|
||||||
|
console.warn('[ClaudeProvider] Hit max turns limit');
|
||||||
|
} else if (
|
||||||
|
message.type === 'result' &&
|
||||||
|
message.subtype === 'error_max_structured_output_retries'
|
||||||
|
) {
|
||||||
|
throw new Error('Failed to produce valid structured output after retries');
|
||||||
|
} else if (message.type === 'error') {
|
||||||
|
const errorMsg = (message as { error?: string }).error || 'Unknown error';
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: responseText, structuredOutput };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,3 +102,92 @@ export interface ModelDefinition {
|
|||||||
tier?: 'basic' | 'standard' | 'premium';
|
tier?: 'basic' | 'standard' | 'premium';
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content block for multi-part prompts (images, structured text)
|
||||||
|
*/
|
||||||
|
export interface TextContentBlock {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageContentBlock {
|
||||||
|
type: 'image';
|
||||||
|
source: {
|
||||||
|
type: 'base64';
|
||||||
|
media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PromptContentBlock = TextContentBlock | ImageContentBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for simple one-shot queries (title generation, descriptions, text enhancement)
|
||||||
|
*
|
||||||
|
* These queries:
|
||||||
|
* - Don't need tools
|
||||||
|
* - Return text directly (no streaming)
|
||||||
|
* - Are single-turn (maxTurns=1)
|
||||||
|
*/
|
||||||
|
export interface SimpleQueryOptions {
|
||||||
|
/** The prompt - either a string or array of content blocks */
|
||||||
|
prompt: string | PromptContentBlock[];
|
||||||
|
|
||||||
|
/** Model to use (defaults to haiku) */
|
||||||
|
model?: string;
|
||||||
|
|
||||||
|
/** Optional system prompt */
|
||||||
|
systemPrompt?: string;
|
||||||
|
|
||||||
|
/** Abort controller for cancellation */
|
||||||
|
abortController?: AbortController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from a simple query
|
||||||
|
*/
|
||||||
|
export interface SimpleQueryResult {
|
||||||
|
/** Extracted text from the response */
|
||||||
|
text: string;
|
||||||
|
|
||||||
|
/** Whether the query completed successfully */
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
/** Error message if failed */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for streaming queries with tools and/or structured output
|
||||||
|
*/
|
||||||
|
export interface StreamingQueryOptions extends SimpleQueryOptions {
|
||||||
|
/** Working directory for tool execution */
|
||||||
|
cwd: string;
|
||||||
|
|
||||||
|
/** Max turns (defaults to sdk-options presets) */
|
||||||
|
maxTurns?: number;
|
||||||
|
|
||||||
|
/** Tools to allow */
|
||||||
|
allowedTools?: readonly string[];
|
||||||
|
|
||||||
|
/** JSON schema for structured output */
|
||||||
|
outputFormat?: {
|
||||||
|
type: 'json_schema';
|
||||||
|
schema: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Callback for text chunks */
|
||||||
|
onText?: (text: string) => void;
|
||||||
|
|
||||||
|
/** Callback for tool usage */
|
||||||
|
onToolUse?: (name: string, input: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from a streaming query with structured output
|
||||||
|
*/
|
||||||
|
export interface StreamingQueryResult extends SimpleQueryResult {
|
||||||
|
/** Parsed structured output if outputFormat was specified */
|
||||||
|
structuredOutput?: unknown;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Generate features from existing app_spec.txt
|
* Generate features from existing app_spec.txt
|
||||||
|
*
|
||||||
|
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { logAuthStatus } from './common.js';
|
|
||||||
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
||||||
import { getAppSpecPath } from '@automaker/platform';
|
import { getAppSpecPath } from '@automaker/platform';
|
||||||
|
|
||||||
@@ -91,72 +91,37 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
|
|||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = createFeatureGenerationOptions({
|
logger.info('Calling provider.executeStreamingQuery() for features...');
|
||||||
|
|
||||||
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
|
const result = await provider.executeStreamingQuery({
|
||||||
|
prompt,
|
||||||
|
model: 'haiku',
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
|
maxTurns: 50,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
abortController,
|
abortController,
|
||||||
|
onText: (text) => {
|
||||||
|
logger.debug(`Feature text block received (${text.length} chars)`);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: text,
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
if (!result.success) {
|
||||||
logger.info('Calling Claude Agent SDK query() for features...');
|
logger.error('❌ Feature generation failed:', result.error);
|
||||||
|
throw new Error(result.error || 'Feature generation failed');
|
||||||
logAuthStatus('Right before SDK query() for features');
|
|
||||||
|
|
||||||
let stream;
|
|
||||||
try {
|
|
||||||
stream = query({ prompt, options });
|
|
||||||
logger.debug('query() returned stream successfully');
|
|
||||||
} catch (queryError) {
|
|
||||||
logger.error('❌ query() threw an exception:');
|
|
||||||
logger.error('Error:', queryError);
|
|
||||||
throw queryError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseText = '';
|
logger.info(`Feature response length: ${result.text.length} chars`);
|
||||||
let messageCount = 0;
|
|
||||||
|
|
||||||
logger.debug('Starting to iterate over feature stream...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const msg of stream) {
|
|
||||||
messageCount++;
|
|
||||||
logger.debug(
|
|
||||||
`Feature stream message #${messageCount}:`,
|
|
||||||
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (msg.type === 'assistant' && msg.message.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'text') {
|
|
||||||
responseText += block.text;
|
|
||||||
logger.debug(`Feature text block received (${block.text.length} chars)`);
|
|
||||||
events.emit('spec-regeneration:event', {
|
|
||||||
type: 'spec_regeneration_progress',
|
|
||||||
content: block.text,
|
|
||||||
projectPath: projectPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
|
||||||
logger.debug('Received success result for features');
|
|
||||||
responseText = (msg as any).result || responseText;
|
|
||||||
} else if ((msg as { type: string }).type === 'error') {
|
|
||||||
logger.error('❌ Received error message from feature stream:');
|
|
||||||
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (streamError) {
|
|
||||||
logger.error('❌ Error while iterating feature stream:');
|
|
||||||
logger.error('Stream error:', streamError);
|
|
||||||
throw streamError;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
|
||||||
logger.info(`Feature response length: ${responseText.length} chars`);
|
|
||||||
logger.info('========== FULL RESPONSE TEXT ==========');
|
logger.info('========== FULL RESPONSE TEXT ==========');
|
||||||
logger.info(responseText);
|
logger.info(result.text);
|
||||||
logger.info('========== END RESPONSE TEXT ==========');
|
logger.info('========== END RESPONSE TEXT ==========');
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, result.text, events);
|
||||||
|
|
||||||
logger.debug('========== generateFeaturesFromSpec() completed ==========');
|
logger.debug('========== generateFeaturesFromSpec() completed ==========');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Generate app_spec.txt from project overview
|
* Generate app_spec.txt from project overview
|
||||||
|
*
|
||||||
|
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import path from 'path';
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
type SpecOutput,
|
type SpecOutput,
|
||||||
} from '../../lib/app-spec-format.js';
|
} from '../../lib/app-spec-format.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { logAuthStatus } from './common.js';
|
|
||||||
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
||||||
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
||||||
|
|
||||||
@@ -83,105 +82,53 @@ ${getStructuredSpecPromptInstruction()}`;
|
|||||||
content: 'Starting spec generation...\n',
|
content: 'Starting spec generation...\n',
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = createSpecGenerationOptions({
|
logger.info('Calling provider.executeStreamingQuery()...');
|
||||||
|
|
||||||
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
|
const result = await provider.executeStreamingQuery({
|
||||||
|
prompt,
|
||||||
|
model: 'haiku',
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
|
maxTurns: 1000,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
abortController,
|
abortController,
|
||||||
outputFormat: {
|
outputFormat: {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
schema: specOutputSchema,
|
schema: specOutputSchema,
|
||||||
},
|
},
|
||||||
|
onText: (text) => {
|
||||||
|
logger.info(`Text block received (${text.length} chars)`);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: text,
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onToolUse: (name, input) => {
|
||||||
|
logger.info('Tool use:', name);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_tool',
|
||||||
|
tool: name,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
if (!result.success) {
|
||||||
logger.info('Calling Claude Agent SDK query()...');
|
logger.error('❌ Spec generation failed:', result.error);
|
||||||
|
throw new Error(result.error || 'Spec generation failed');
|
||||||
// Log auth status right before the SDK call
|
|
||||||
logAuthStatus('Right before SDK query()');
|
|
||||||
|
|
||||||
let stream;
|
|
||||||
try {
|
|
||||||
stream = query({ prompt, options });
|
|
||||||
logger.debug('query() returned stream successfully');
|
|
||||||
} catch (queryError) {
|
|
||||||
logger.error('❌ query() threw an exception:');
|
|
||||||
logger.error('Error:', queryError);
|
|
||||||
throw queryError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseText = '';
|
const responseText = result.text;
|
||||||
let messageCount = 0;
|
const structuredOutput = result.structuredOutput as SpecOutput | undefined;
|
||||||
let structuredOutput: SpecOutput | null = null;
|
|
||||||
|
|
||||||
logger.info('Starting to iterate over stream...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const msg of stream) {
|
|
||||||
messageCount++;
|
|
||||||
logger.info(
|
|
||||||
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (msg.type === 'assistant') {
|
|
||||||
const msgAny = msg as any;
|
|
||||||
if (msgAny.message?.content) {
|
|
||||||
for (const block of msgAny.message.content) {
|
|
||||||
if (block.type === 'text') {
|
|
||||||
responseText += block.text;
|
|
||||||
logger.info(
|
|
||||||
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
|
||||||
);
|
|
||||||
events.emit('spec-regeneration:event', {
|
|
||||||
type: 'spec_regeneration_progress',
|
|
||||||
content: block.text,
|
|
||||||
projectPath: projectPath,
|
|
||||||
});
|
|
||||||
} else if (block.type === 'tool_use') {
|
|
||||||
logger.info('Tool use:', block.name);
|
|
||||||
events.emit('spec-regeneration:event', {
|
|
||||||
type: 'spec_tool',
|
|
||||||
tool: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
|
||||||
logger.info('Received success result');
|
|
||||||
// Check for structured output - this is the reliable way to get spec data
|
|
||||||
const resultMsg = msg as any;
|
|
||||||
if (resultMsg.structured_output) {
|
|
||||||
structuredOutput = resultMsg.structured_output as SpecOutput;
|
|
||||||
logger.info('✅ Received structured output');
|
|
||||||
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
|
|
||||||
} else {
|
|
||||||
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result') {
|
|
||||||
// Handle error result types
|
|
||||||
const subtype = (msg as any).subtype;
|
|
||||||
logger.info(`Result message: subtype=${subtype}`);
|
|
||||||
if (subtype === 'error_max_turns') {
|
|
||||||
logger.error('❌ Hit max turns limit!');
|
|
||||||
} else if (subtype === 'error_max_structured_output_retries') {
|
|
||||||
logger.error('❌ Failed to produce valid structured output after retries');
|
|
||||||
throw new Error('Could not produce valid spec output');
|
|
||||||
}
|
|
||||||
} else if ((msg as { type: string }).type === 'error') {
|
|
||||||
logger.error('❌ Received error message from stream:');
|
|
||||||
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
|
||||||
} else if (msg.type === 'user') {
|
|
||||||
// Log user messages (tool results)
|
|
||||||
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (streamError) {
|
|
||||||
logger.error('❌ Error while iterating stream:');
|
|
||||||
logger.error('Stream error:', streamError);
|
|
||||||
throw streamError;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
|
||||||
logger.info(`Response text length: ${responseText.length} chars`);
|
logger.info(`Response text length: ${responseText.length} chars`);
|
||||||
|
if (structuredOutput) {
|
||||||
|
logger.info('✅ Received structured output');
|
||||||
|
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
|
||||||
|
}
|
||||||
|
|
||||||
// Determine XML content to save
|
// Determine XML content to save
|
||||||
let xmlContent: string;
|
let xmlContent: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* POST /context/describe-file endpoint - Generate description for a text file
|
* POST /context/describe-file endpoint - Generate description for a text file
|
||||||
*
|
*
|
||||||
* Uses Claude Haiku to analyze a text file and generate a concise description
|
* Uses Claude Haiku via ClaudeProvider to analyze a text file and generate
|
||||||
* suitable for context file metadata.
|
* a concise description suitable for context file metadata.
|
||||||
*
|
*
|
||||||
* SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY
|
* SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY
|
||||||
* and reads file content directly (not via Claude's Read tool) to prevent
|
* and reads file content directly (not via Claude's Read tool) to prevent
|
||||||
@@ -10,11 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
|
||||||
import { PathNotAllowedError } from '@automaker/platform';
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { createCustomOptions } from '../../../lib/sdk-options.js';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@@ -44,31 +42,6 @@ interface DescribeFileErrorResponse {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text content from Claude SDK response messages
|
|
||||||
*/
|
|
||||||
async function extractTextFromStream(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
stream: AsyncIterable<any>
|
|
||||||
): Promise<string> {
|
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
|
|
||||||
for (const block of blocks) {
|
|
||||||
if (block.type === 'text' && block.text) {
|
|
||||||
responseText += block.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
|
||||||
responseText = msg.result || responseText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the describe-file request handler
|
* Create the describe-file request handler
|
||||||
*
|
*
|
||||||
@@ -150,60 +123,39 @@ export function createDescribeFileHandler(): (req: Request, res: Response) => Pr
|
|||||||
const fileName = path.basename(resolvedPath);
|
const fileName = path.basename(resolvedPath);
|
||||||
|
|
||||||
// Build prompt with file content passed as structured data
|
// Build prompt with file content passed as structured data
|
||||||
// The file content is included directly, not via tool invocation
|
const promptContent = [
|
||||||
const instructionText = `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
|
||||||
|
|
||||||
Respond with ONLY the description text, no additional formatting, preamble, or explanation.
|
Respond with ONLY the description text, no additional formatting, preamble, or explanation.
|
||||||
|
|
||||||
File: ${fileName}${truncated ? ' (truncated)' : ''}`;
|
File: ${fileName}${truncated ? ' (truncated)' : ''}`,
|
||||||
|
},
|
||||||
const promptContent = [
|
|
||||||
{ type: 'text' as const, text: instructionText },
|
|
||||||
{ type: 'text' as const, text: `\n\n--- FILE CONTENT ---\n${contentToAnalyze}` },
|
{ type: 'text' as const, text: `\n\n--- FILE CONTENT ---\n${contentToAnalyze}` },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Use the file's directory as the working directory
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
const cwd = path.dirname(resolvedPath);
|
const result = await provider.executeSimpleQuery({
|
||||||
|
prompt: promptContent,
|
||||||
// Use centralized SDK options with proper cwd validation
|
model: 'haiku',
|
||||||
// No tools needed since we're passing file content directly
|
|
||||||
const sdkOptions = createCustomOptions({
|
|
||||||
cwd,
|
|
||||||
model: CLAUDE_MODEL_MAP.haiku,
|
|
||||||
maxTurns: 1,
|
|
||||||
allowedTools: [],
|
|
||||||
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const promptGenerator = (async function* () {
|
if (!result.success) {
|
||||||
yield {
|
logger.warn('Failed to generate description:', result.error);
|
||||||
type: 'user' as const,
|
|
||||||
session_id: '',
|
|
||||||
message: { role: 'user' as const, content: promptContent },
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
const stream = query({ prompt: promptGenerator, options: sdkOptions });
|
|
||||||
|
|
||||||
// Extract the description from the response
|
|
||||||
const description = await extractTextFromStream(stream);
|
|
||||||
|
|
||||||
if (!description || description.trim().length === 0) {
|
|
||||||
logger.warn('Received empty response from Claude');
|
|
||||||
const response: DescribeFileErrorResponse = {
|
const response: DescribeFileErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to generate description - empty response',
|
error: result.error || 'Failed to generate description',
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Description generated, length: ${description.length} chars`);
|
logger.info(`Description generated, length: ${result.text.length} chars`);
|
||||||
|
|
||||||
const response: DescribeFileSuccessResponse = {
|
const response: DescribeFileSuccessResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
description: description.trim(),
|
description: result.text,
|
||||||
};
|
};
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* POST /context/describe-image endpoint - Generate description for an image
|
* POST /context/describe-image endpoint - Generate description for an image
|
||||||
*
|
*
|
||||||
* Uses Claude Haiku to analyze an image and generate a concise description
|
* Uses Claude Haiku via ClaudeProvider to analyze an image and generate
|
||||||
* suitable for context file metadata.
|
* a concise description suitable for context file metadata.
|
||||||
*
|
*
|
||||||
* IMPORTANT:
|
* IMPORTANT:
|
||||||
* The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks),
|
* The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks),
|
||||||
@@ -11,10 +11,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import { createLogger, readImageAsBase64 } from '@automaker/utils';
|
import { createLogger, readImageAsBase64 } from '@automaker/utils';
|
||||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import { createCustomOptions } from '../../../lib/sdk-options.js';
|
import type { PromptContentBlock } from '../../../providers/types.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@@ -173,53 +172,6 @@ function mapDescribeImageError(rawMessage: string | undefined): {
|
|||||||
return baseResponse;
|
return baseResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text content from Claude SDK response messages and log high-signal stream events.
|
|
||||||
*/
|
|
||||||
async function extractTextFromStream(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
stream: AsyncIterable<any>,
|
|
||||||
requestId: string
|
|
||||||
): Promise<string> {
|
|
||||||
let responseText = '';
|
|
||||||
let messageCount = 0;
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] [Stream] Begin reading SDK stream...`);
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
|
||||||
messageCount++;
|
|
||||||
const msgType = msg?.type;
|
|
||||||
const msgSubtype = msg?.subtype;
|
|
||||||
|
|
||||||
// Keep this concise but informative. Full error object is logged in catch blocks.
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] [Stream] #${messageCount} type=${String(msgType)} subtype=${String(msgSubtype ?? '')}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (msgType === 'assistant' && msg.message?.content) {
|
|
||||||
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
|
|
||||||
logger.info(`[${requestId}] [Stream] assistant blocks=${blocks.length}`);
|
|
||||||
for (const block of blocks) {
|
|
||||||
if (block.type === 'text' && block.text) {
|
|
||||||
responseText += block.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msgType === 'result' && msgSubtype === 'success') {
|
|
||||||
if (typeof msg.result === 'string' && msg.result.length > 0) {
|
|
||||||
responseText = msg.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[${requestId}] [Stream] End of stream. messages=${messageCount} textLength=${responseText.length}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the describe-image request handler
|
* Create the describe-image request handler
|
||||||
*
|
*
|
||||||
@@ -308,13 +260,17 @@ export function createDescribeImageHandler(): (req: Request, res: Response) => P
|
|||||||
`"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
|
`"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
|
||||||
`Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
|
`Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
|
||||||
|
|
||||||
const promptContent = [
|
const promptContent: PromptContentBlock[] = [
|
||||||
{ type: 'text' as const, text: instructionText },
|
{ type: 'text', text: instructionText },
|
||||||
{
|
{
|
||||||
type: 'image' as const,
|
type: 'image',
|
||||||
source: {
|
source: {
|
||||||
type: 'base64' as const,
|
type: 'base64',
|
||||||
media_type: imageData.mimeType,
|
media_type: imageData.mimeType as
|
||||||
|
| 'image/jpeg'
|
||||||
|
| 'image/png'
|
||||||
|
| 'image/gif'
|
||||||
|
| 'image/webp',
|
||||||
data: imageData.base64,
|
data: imageData.base64,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -322,48 +278,26 @@ export function createDescribeImageHandler(): (req: Request, res: Response) => P
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Built multi-part prompt blocks=${promptContent.length}`);
|
logger.info(`[${requestId}] Built multi-part prompt blocks=${promptContent.length}`);
|
||||||
|
|
||||||
const cwd = path.dirname(actualPath);
|
logger.info(`[${requestId}] Calling provider.executeSimpleQuery()...`);
|
||||||
logger.info(`[${requestId}] Using cwd=${cwd}`);
|
const queryStart = Date.now();
|
||||||
|
|
||||||
// Use the same centralized option builder used across the server (validates cwd)
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
const sdkOptions = createCustomOptions({
|
const result = await provider.executeSimpleQuery({
|
||||||
cwd,
|
prompt: promptContent,
|
||||||
model: CLAUDE_MODEL_MAP.haiku,
|
model: 'haiku',
|
||||||
maxTurns: 1,
|
|
||||||
allowedTools: [],
|
|
||||||
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(`[${requestId}] Query completed in ${Date.now() - queryStart}ms`);
|
||||||
`[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify(
|
|
||||||
sdkOptions.allowedTools
|
|
||||||
)} sandbox=${JSON.stringify(sdkOptions.sandbox)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptGenerator = (async function* () {
|
const description = result.success ? result.text : '';
|
||||||
yield {
|
|
||||||
type: 'user' as const,
|
|
||||||
session_id: '',
|
|
||||||
message: { role: 'user' as const, content: promptContent },
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Calling query()...`);
|
if (!result.success || !description || description.trim().length === 0) {
|
||||||
const queryStart = Date.now();
|
logger.warn(
|
||||||
const stream = query({ prompt: promptGenerator, options: sdkOptions });
|
`[${requestId}] Failed to generate description: ${result.error || 'empty response'}`
|
||||||
logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`);
|
);
|
||||||
|
|
||||||
// Extract the description from the response
|
|
||||||
const extractStart = Date.now();
|
|
||||||
const description = await extractTextFromStream(stream, requestId);
|
|
||||||
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`);
|
|
||||||
|
|
||||||
if (!description || description.trim().length === 0) {
|
|
||||||
logger.warn(`[${requestId}] Received empty response from Claude`);
|
|
||||||
const response: DescribeImageErrorResponse = {
|
const response: DescribeImageErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to generate description - empty response',
|
error: result.error || 'Failed to generate description - empty response',
|
||||||
requestId,
|
requestId,
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* POST /enhance-prompt endpoint - Enhance user input text
|
* POST /enhance-prompt endpoint - Enhance user input text
|
||||||
*
|
*
|
||||||
* Uses Claude AI to enhance text based on the specified enhancement mode.
|
* Uses Claude AI via ClaudeProvider to enhance text based on the specified
|
||||||
* Supports modes: improve, technical, simplify, acceptance
|
* enhancement mode. Supports modes: improve, technical, simplify, acceptance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { resolveModelString } from '@automaker/model-resolver';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
|
||||||
import {
|
import {
|
||||||
getSystemPrompt,
|
getSystemPrompt,
|
||||||
buildUserPrompt,
|
buildUserPrompt,
|
||||||
@@ -47,39 +45,6 @@ interface EnhanceErrorResponse {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text content from Claude SDK response messages
|
|
||||||
*
|
|
||||||
* @param stream - The async iterable from the query function
|
|
||||||
* @returns The extracted text content
|
|
||||||
*/
|
|
||||||
async function extractTextFromStream(
|
|
||||||
stream: AsyncIterable<{
|
|
||||||
type: string;
|
|
||||||
subtype?: string;
|
|
||||||
result?: string;
|
|
||||||
message?: {
|
|
||||||
content?: Array<{ type: string; text?: string }>;
|
|
||||||
};
|
|
||||||
}>
|
|
||||||
): Promise<string> {
|
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'text' && block.text) {
|
|
||||||
responseText += block.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
|
||||||
responseText = msg.result || responseText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the enhance request handler
|
* Create the enhance request handler
|
||||||
*
|
*
|
||||||
@@ -132,45 +97,30 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
|
|||||||
const systemPrompt = getSystemPrompt(validMode);
|
const systemPrompt = getSystemPrompt(validMode);
|
||||||
|
|
||||||
// Build the user prompt with few-shot examples
|
// Build the user prompt with few-shot examples
|
||||||
// This helps the model understand this is text transformation, not a coding task
|
|
||||||
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
|
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
|
||||||
|
|
||||||
// Resolve the model - use the passed model, default to sonnet for quality
|
const provider = ProviderFactory.getProviderForModel(model || 'sonnet');
|
||||||
const resolvedModel = resolveModelString(model, CLAUDE_MODEL_MAP.sonnet);
|
const result = await provider.executeSimpleQuery({
|
||||||
|
|
||||||
logger.debug(`Using model: ${resolvedModel}`);
|
|
||||||
|
|
||||||
// Call Claude SDK with minimal configuration for text transformation
|
|
||||||
// Key: no tools, just text completion
|
|
||||||
const stream = query({
|
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
options: {
|
model: model || 'sonnet',
|
||||||
model: resolvedModel,
|
systemPrompt,
|
||||||
systemPrompt,
|
|
||||||
maxTurns: 1,
|
|
||||||
allowedTools: [],
|
|
||||||
permissionMode: 'acceptEdits',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract the enhanced text from the response
|
if (!result.success) {
|
||||||
const enhancedText = await extractTextFromStream(stream);
|
logger.warn('Failed to enhance text:', result.error);
|
||||||
|
|
||||||
if (!enhancedText || enhancedText.trim().length === 0) {
|
|
||||||
logger.warn('Received empty response from Claude');
|
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to generate enhanced text - empty response',
|
error: result.error || 'Failed to generate enhanced text',
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Enhancement complete, output length: ${enhancedText.length} chars`);
|
logger.info(`Enhancement complete, output length: ${result.text.length} chars`);
|
||||||
|
|
||||||
const response: EnhanceSuccessResponse = {
|
const response: EnhanceSuccessResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
enhancedText: enhancedText.trim(),
|
enhancedText: result.text,
|
||||||
};
|
};
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* POST /features/generate-title endpoint - Generate a concise title from description
|
* POST /features/generate-title endpoint - Generate a concise title from description
|
||||||
*
|
*
|
||||||
* Uses Claude Haiku to generate a short, descriptive title from feature description.
|
* Uses Claude Haiku via ClaudeProvider to generate a short, descriptive title.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
|
|
||||||
const logger = createLogger('GenerateTitle');
|
const logger = createLogger('GenerateTitle');
|
||||||
|
|
||||||
@@ -34,33 +33,6 @@ Rules:
|
|||||||
- No quotes, periods, or extra formatting
|
- No quotes, periods, or extra formatting
|
||||||
- Capture the essence of the feature in a scannable way`;
|
- Capture the essence of the feature in a scannable way`;
|
||||||
|
|
||||||
async function extractTextFromStream(
|
|
||||||
stream: AsyncIterable<{
|
|
||||||
type: string;
|
|
||||||
subtype?: string;
|
|
||||||
result?: string;
|
|
||||||
message?: {
|
|
||||||
content?: Array<{ type: string; text?: string }>;
|
|
||||||
};
|
|
||||||
}>
|
|
||||||
): Promise<string> {
|
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'text' && block.text) {
|
|
||||||
responseText += block.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
|
||||||
responseText = msg.result || responseText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
|
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -89,34 +61,28 @@ export function createGenerateTitleHandler(): (req: Request, res: Response) => P
|
|||||||
|
|
||||||
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
|
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
|
||||||
|
|
||||||
const stream = query({
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
|
const result = await provider.executeSimpleQuery({
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
options: {
|
model: 'haiku',
|
||||||
model: CLAUDE_MODEL_MAP.haiku,
|
systemPrompt: SYSTEM_PROMPT,
|
||||||
systemPrompt: SYSTEM_PROMPT,
|
|
||||||
maxTurns: 1,
|
|
||||||
allowedTools: [],
|
|
||||||
permissionMode: 'acceptEdits',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = await extractTextFromStream(stream);
|
if (!result.success) {
|
||||||
|
logger.warn('Failed to generate title:', result.error);
|
||||||
if (!title || title.trim().length === 0) {
|
|
||||||
logger.warn('Received empty response from Claude');
|
|
||||||
const response: GenerateTitleErrorResponse = {
|
const response: GenerateTitleErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to generate title - empty response',
|
error: result.error || 'Failed to generate title',
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Generated title: ${title.trim()}`);
|
logger.info(`Generated title: ${result.text}`);
|
||||||
|
|
||||||
const response: GenerateTitleSuccessResponse = {
|
const response: GenerateTitleSuccessResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
title: title.trim(),
|
title: result.text,
|
||||||
};
|
};
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Business logic for generating suggestions
|
* Business logic for generating suggestions
|
||||||
|
*
|
||||||
|
* Uses ClaudeProvider.executeStreamingQuery() for SDK interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { createSuggestionsOptions } from '../../lib/sdk-options.js';
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
|
|
||||||
const logger = createLogger('Suggestions');
|
const logger = createLogger('Suggestions');
|
||||||
|
|
||||||
@@ -68,62 +69,44 @@ The response will be automatically formatted as structured JSON.`;
|
|||||||
content: `Starting ${suggestionType} analysis...\n`,
|
content: `Starting ${suggestionType} analysis...\n`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = createSuggestionsOptions({
|
const provider = ProviderFactory.getProviderForModel('haiku');
|
||||||
|
const result = await provider.executeStreamingQuery({
|
||||||
|
prompt,
|
||||||
|
model: 'haiku',
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
|
maxTurns: 250,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
abortController,
|
abortController,
|
||||||
outputFormat: {
|
outputFormat: {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
schema: suggestionsSchema,
|
schema: suggestionsSchema,
|
||||||
},
|
},
|
||||||
|
onText: (text) => {
|
||||||
|
events.emit('suggestions:event', {
|
||||||
|
type: 'suggestions_progress',
|
||||||
|
content: text,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onToolUse: (name, input) => {
|
||||||
|
events.emit('suggestions:event', {
|
||||||
|
type: 'suggestions_tool',
|
||||||
|
tool: name,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const stream = query({ prompt, options });
|
|
||||||
let responseText = '';
|
|
||||||
let structuredOutput: { suggestions: Array<Record<string, unknown>> } | null = null;
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === 'assistant' && msg.message.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'text') {
|
|
||||||
responseText += block.text;
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_progress',
|
|
||||||
content: block.text,
|
|
||||||
});
|
|
||||||
} else if (block.type === 'tool_use') {
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_tool',
|
|
||||||
tool: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
|
||||||
// Check for structured output
|
|
||||||
const resultMsg = msg as any;
|
|
||||||
if (resultMsg.structured_output) {
|
|
||||||
structuredOutput = resultMsg.structured_output as {
|
|
||||||
suggestions: Array<Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
logger.debug('Received structured output:', structuredOutput);
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result') {
|
|
||||||
const resultMsg = msg as any;
|
|
||||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
|
||||||
logger.error('Failed to produce valid structured output after retries');
|
|
||||||
throw new Error('Could not produce valid suggestions output');
|
|
||||||
} else if (resultMsg.subtype === 'error_max_turns') {
|
|
||||||
logger.error('Hit max turns limit before completing suggestions generation');
|
|
||||||
logger.warn(`Response text length: ${responseText.length} chars`);
|
|
||||||
// Still try to parse what we have
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use structured output if available, otherwise fall back to parsing text
|
// Use structured output if available, otherwise fall back to parsing text
|
||||||
try {
|
try {
|
||||||
|
const structuredOutput = result.structuredOutput as
|
||||||
|
| {
|
||||||
|
suggestions: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
if (structuredOutput && structuredOutput.suggestions) {
|
if (structuredOutput && structuredOutput.suggestions) {
|
||||||
// Use structured output directly
|
// Use structured output directly
|
||||||
|
logger.debug('Received structured output:', structuredOutput);
|
||||||
events.emit('suggestions:event', {
|
events.emit('suggestions:event', {
|
||||||
type: 'suggestions_complete',
|
type: 'suggestions_complete',
|
||||||
suggestions: structuredOutput.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
suggestions: structuredOutput.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
||||||
@@ -134,7 +117,7 @@ The response will be automatically formatted as structured JSON.`;
|
|||||||
} else {
|
} else {
|
||||||
// Fallback: try to parse from text (for backwards compatibility)
|
// Fallback: try to parse from text (for backwards compatibility)
|
||||||
logger.warn('No structured output received, attempting to parse from text');
|
logger.warn('No structured output received, attempting to parse from text');
|
||||||
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
|
const jsonMatch = result.text.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
|
||||||
if (jsonMatch) {
|
if (jsonMatch) {
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
events.emit('suggestions:event', {
|
events.emit('suggestions:event', {
|
||||||
|
|||||||
Reference in New Issue
Block a user