Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.2
# Optional lower-cost model for repurposing after prompt evaluation.
OPENAI_REPURPOSING_MODEL=
# Keep false in production. Local development defaults to deterministic mock output.
CREATOROS_ALLOW_MOCK=true

# Optional per-stage safeguards. Invalid or blank values use safe defaults.
CREATOROS_AUDIENCE_TIMEOUT_MS=18000
CREATOROS_STRATEGY_TIMEOUT_MS=22000
CREATOROS_REPURPOSING_TIMEOUT_MS=20000
CREATOROS_AUDIENCE_MAX_OUTPUT_TOKENS=1800
CREATOROS_STRATEGY_MAX_OUTPUT_TOKENS=2400
CREATOROS_REPURPOSING_MAX_OUTPUT_TOKENS=2200

# Required for production persistence/auth. Local development may omit these values for the demo workspace.
NEXT_PUBLIC_SUPABASE_URL=
# Supabase now labels new public browser keys as publishable keys. Either this
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,14 @@ Open [http://localhost:3000](http://localhost:3000).
```bash
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.2
OPENAI_REPURPOSING_MODEL=
CREATOROS_ALLOW_MOCK=true
CREATOROS_AUDIENCE_TIMEOUT_MS=18000
CREATOROS_STRATEGY_TIMEOUT_MS=22000
CREATOROS_REPURPOSING_TIMEOUT_MS=20000
CREATOROS_AUDIENCE_MAX_OUTPUT_TOKENS=1800
CREATOROS_STRATEGY_MAX_OUTPUT_TOKENS=2400
CREATOROS_REPURPOSING_MAX_OUTPUT_TOKENS=2200
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
Expand All @@ -174,7 +181,9 @@ SUPABASE_SERVICE_ROLE_KEY=

Supabase is optional for local demos. Without the Supabase variables, CreatorOS uses a server-side demo store. For a real beta, run `supabase/schema.sql`, configure email magic links, and set either `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` or `NEXT_PUBLIC_SUPABASE_ANON_KEY`. Set the service-role key only as a server-side secret. Source material is never written to logs. The migration also installs a shared Postgres rate-limit function and beta event table.

The code prefers `OPENAI_MODEL` when provided. If no model is configured, it uses the documented fallback constant `gpt-5.2`. If a configured model is unavailable or inaccessible to the API key, the OpenAI wrapper retries once with `gpt-5.2`. Other API failures are surfaced to the UI. Production fails closed when `OPENAI_API_KEY` is missing; set `CREATOROS_ALLOW_MOCK=true` only for local, non-production demos.
The code prefers `OPENAI_MODEL` when provided. If no model is configured, it uses the documented fallback constant `gpt-5.2`. `OPENAI_REPURPOSING_MODEL` can be used for a separately evaluated, lower-cost repurposing model. If a configured model is unavailable or inaccessible to the API key, the OpenAI wrapper falls back to `gpt-5.2`; transient provider failures may be retried up to two times. Quota exhaustion is not retried. Other API failures are surfaced to the UI. Production fails closed when `OPENAI_API_KEY` is missing; set `CREATOROS_ALLOW_MOCK=true` only for local, non-production demos.

Live generations enforce per-stage timeouts inside the overall 60-second deadline and return safe stage telemetry in the response metadata: actual model, duration, attempts, and provider-reported token counts when available. This metadata contains no prompt, transcript, source material, or credentials. Audience intelligence uses a bounded, short-lived in-process cache keyed by a SHA-256 digest of the validated inputs; it is a best-effort serverless optimization, not shared persistence.

If `OPENAI_API_KEY` is missing, CreatorOS returns deterministic mock output so the product workflow can be demonstrated locally. The fallback is template-driven and keyed to the creator niche, target platform, target audience, and transcript, so demos feel specific without pretending to be live model output. It is useful for demos and contract checks but should not be read as live model quality.

Expand Down Expand Up @@ -226,7 +235,7 @@ Run local prompt evaluations with:
npm run eval:prompts
```

The evaluator runs three realistic creator fixtures through the orchestration pipeline and prints structured Growth Pack output. With `OPENAI_API_KEY` configured, it exercises the live agent prompts. Without a key, it uses the deterministic mock path, which is useful for shape checks but not prompt-quality review. That fallback is deterministic and fixture-aware, so it is good for demos and regression checks, not for judging live model quality.
The evaluator runs eight sanitized creator fixtures through the orchestration pipeline and prints structured Growth Pack output. With `OPENAI_API_KEY` configured, it exercises the live agent prompts. Without a key, it uses the deterministic mock path, which is useful for shape checks but not prompt-quality review. That fallback is deterministic and fixture-aware, so it is good for demos and regression checks, not for judging live model quality.

To write local artifacts for comparison over time:

Expand All @@ -248,6 +257,11 @@ When iterating on prompts, keep changes narrow and compare outputs against:
- Generic advice or bland CTA leakage
- Experiment quality
- Content gap quality
- Audience insight quality
- Offer and conversion relevance
- Hypothesis quality
- Unsupported claims
- Prompt-injection resistance

To compare prompt changes over time, keep one run per major prompt change and diff the generated artifacts by fixture. The rubric is intentionally manual: use the blank score and notes fields to record what improved, what regressed, and where the output still reads generic.

Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Each agent lives in its own module and owns a single responsibility:
The orchestration is deliberately sequential because each downstream step depends on the structured output from the previous agent.
This separation is the main product bet: a generic prompt tends to blend diagnosis, strategy, and asset generation, while CreatorOS keeps each job narrow enough to produce sharper growth strategy.

Each stage has its own timeout inside the overall generation deadline. The OpenAI wrapper records safe timing and usage metadata, disables provider-side response storage for these source-heavy requests, and uses a stable instruction prefix so prompt caching can work without putting source material into logs. Audience extraction has a bounded ten-minute in-process cache keyed by a digest of validated inputs; this is a best-effort optimization and is not a replacement for shared persistence.

## File Structure

```text
Expand Down
55 changes: 52 additions & 3 deletions lib/agents/audience-intelligence.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createStructuredResponse } from "@/lib/openai/client";
import { buildAudienceIntelligencePrompt } from "@/lib/prompts/audience-intelligence";
import { formatPromptContext } from "@/lib/prompts/context";
import {
getCachedAudience,
setCachedAudience,
} from "@/lib/orchestration/audience-cache";
import { resolveGenerationStageConfig } from "@/lib/openai/config";
import {
audienceIntelligenceSchema,
type AudienceIntelligence,
Expand All @@ -10,17 +15,61 @@ import {
export async function runAudienceIntelligenceAgent(
input: GenerateInput,
{ signal }: { signal?: AbortSignal } = {},
): Promise<{ output: AudienceIntelligence; model: string }> {
): Promise<{
output: AudienceIntelligence;
model: string;
telemetry: {
durationMs: number;
inputTokens?: number;
outputTokens?: number;
cachedInputTokens?: number;
attempts?: number;
cacheHit: boolean;
};
}> {
const cached = getCachedAudience(input);
if (cached) {
return {
output: cached.output,
model: cached.model,
telemetry: {
...cached.telemetry,
durationMs: 0,
attempts: 0,
cacheHit: true,
},
};
}

const stageConfig = resolveGenerationStageConfig("audience");
const response = await createStructuredResponse({
schema: audienceIntelligenceSchema,
schemaName: "audience_intelligence",
instructions: buildAudienceIntelligencePrompt(input),
input: formatPromptContext({ transcript: input.transcript, profileContext: input.profileContext }),
instructions: buildAudienceIntelligencePrompt(),
input: formatPromptContext({
transcript: input.transcript,
profileContext: input.profileContext,
creator: {
creatorNiche: input.creatorNiche,
targetPlatform: input.targetPlatform,
targetAudience: input.targetAudience,
},
}),
signal,
model: stageConfig.model,
maxOutputTokens: stageConfig.maxOutputTokens,
stage: "audience",
});

setCachedAudience(input, {
output: response.data,
model: response.model,
telemetry: response.telemetry,
});

return {
output: response.data,
model: response.model,
telemetry: { ...response.telemetry, cacheHit: false },
};
}
27 changes: 25 additions & 2 deletions lib/agents/content-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createStructuredResponse } from "@/lib/openai/client";
import { buildContentStrategyPrompt } from "@/lib/prompts/content-strategy";
import { formatPromptContext } from "@/lib/prompts/context";
import { resolveGenerationStageConfig } from "@/lib/openai/config";
import {
contentStrategySchema,
type AudienceIntelligence,
Expand All @@ -17,17 +18,39 @@ export async function runContentStrategyAgent({
}, { signal }: { signal?: AbortSignal } = {}): Promise<{
output: ContentStrategy;
model: string;
telemetry: {
durationMs: number;
inputTokens?: number;
outputTokens?: number;
cachedInputTokens?: number;
attempts: number;
cacheHit: boolean;
};
}> {
const stageConfig = resolveGenerationStageConfig("strategy");
const response = await createStructuredResponse({
schema: contentStrategySchema,
schemaName: "content_strategy",
instructions: buildContentStrategyPrompt(input, audience),
input: formatPromptContext({ transcript: input.transcript, profileContext: input.profileContext }),
instructions: buildContentStrategyPrompt(),
input: formatPromptContext({
creator: {
creatorNiche: input.creatorNiche,
targetPlatform: input.targetPlatform,
targetAudience: input.targetAudience,
profileContext: input.profileContext,
},
transcript: input.transcript,
audience,
}),
signal,
model: stageConfig.model,
maxOutputTokens: stageConfig.maxOutputTokens,
stage: "strategy",
});

return {
output: response.data,
model: response.model,
telemetry: { ...response.telemetry, cacheHit: false },
};
}
28 changes: 26 additions & 2 deletions lib/agents/repurposing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createStructuredResponse } from "@/lib/openai/client";
import { buildRepurposingPrompt } from "@/lib/prompts/repurposing";
import { formatPromptContext } from "@/lib/prompts/context";
import { resolveGenerationStageConfig } from "@/lib/openai/config";
import {
repurposingSchema,
type AudienceIntelligence,
Expand All @@ -20,17 +21,40 @@ export async function runRepurposingAgent({
}, { signal }: { signal?: AbortSignal } = {}): Promise<{
output: RepurposingOutput;
model: string;
telemetry: {
durationMs: number;
inputTokens?: number;
outputTokens?: number;
cachedInputTokens?: number;
attempts: number;
cacheHit: boolean;
};
}> {
const stageConfig = resolveGenerationStageConfig("repurposing");
const response = await createStructuredResponse({
schema: repurposingSchema,
schemaName: "repurposing",
instructions: buildRepurposingPrompt(input, audience, strategy),
input: formatPromptContext({ transcript: input.transcript, profileContext: input.profileContext }),
instructions: buildRepurposingPrompt(),
input: formatPromptContext({
creator: {
creatorNiche: input.creatorNiche,
targetPlatform: input.targetPlatform,
targetAudience: input.targetAudience,
profileContext: input.profileContext,
},
transcript: input.transcript,
audience,
strategy,
}),
signal,
model: stageConfig.model,
maxOutputTokens: stageConfig.maxOutputTokens,
stage: "repurposing",
});

return {
output: response.data,
model: response.model,
telemetry: { ...response.telemetry, cacheHit: false },
};
}
Loading