Skip to content

Commit 771b99f

Browse files
Aitomatesclaude
andcommitted
fix: apply audit code-review fixes — semantic provider, structured responses, core refinements
Prior audit session (F-01 through F-08) left code fixes unstaged. Includes semantic-provider.ts, structured-response.ts, and updates to config, dashboard, server, history, lesson extraction, and refiners. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6402966 commit 771b99f

18 files changed

Lines changed: 681 additions & 94 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ cd Promptimprover
7979

8080
Both installers perform a deterministic dependency install, run the full test suite, build the package, install it globally, and verify the `gemini-prompt-refiner` command. Add that command to your MCP client configuration. See the [Setup Guide](https://github.com/Coding-Autopilot-System/Promptimprover/wiki/Setup-Guide) for full configuration instructions.
8181

82+
## Local Semantic Model
83+
84+
PromptImprover uses a local OpenAI-compatible endpoint before optional MCP sampling. The safe defaults target `http://localhost:9000/v1`, use `gemma3:12b` first, and fall back to `gemma3:1b`. If neither local model nor MCP sampling is available, rule-based refinement continues without semantic output.
85+
86+
Override the defaults per repository with `.gemini-refiner.json`:
87+
88+
```json
89+
{
90+
"semantic": {
91+
"localEnabled": true,
92+
"mcpSamplingEnabled": true,
93+
"baseUrl": "http://localhost:9000/v1",
94+
"models": ["gemma3:12b", "gemma3:1b"],
95+
"timeoutMs": 120000,
96+
"temperature": 0.2,
97+
"allowNonLoopback": false
98+
}
99+
}
100+
```
101+
102+
Non-loopback model endpoints are rejected unless `allowNonLoopback` is explicitly enabled. Generated lessons and templates remain pending until reviewed through the MCP learning-review tools.
103+
82104
## License
83105

84106
MIT - see [LICENSE](LICENSE)

universal-refiner/src/core/config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,30 @@ import { AgenticBlackboard } from "./blackboard.js";
55
export interface RefinerConfig {
66
mandates?: string[];
77
ignoredPaths?: string[];
8+
semantic?: Partial<SemanticConfig>;
9+
}
10+
11+
export interface SemanticConfig {
12+
localEnabled: boolean;
13+
mcpSamplingEnabled: boolean;
14+
baseUrl: string;
15+
models: string[];
16+
timeoutMs: number;
17+
temperature: number;
18+
allowNonLoopback: boolean;
819
}
920

1021
export class ConfigManager {
1122
private static CONFIG_FILE = ".gemini-refiner.json";
23+
private static DEFAULT_SEMANTIC_CONFIG: SemanticConfig = {
24+
localEnabled: true,
25+
mcpSamplingEnabled: true,
26+
baseUrl: "http://localhost:9000/v1",
27+
models: ["gemma3:12b", "gemma3:1b"],
28+
timeoutMs: 120000,
29+
temperature: 0.2,
30+
allowNonLoopback: false,
31+
};
1232

1333
static loadConfig(rootPath: string = "."): RefinerConfig {
1434
const configPath = path.join(rootPath, this.CONFIG_FILE);
@@ -25,6 +45,26 @@ export class ConfigManager {
2545
}
2646
}
2747

48+
static getSemanticConfig(rootPath: string = "."): SemanticConfig {
49+
const semantic = this.loadConfig(rootPath).semantic || {};
50+
const defaults = this.DEFAULT_SEMANTIC_CONFIG;
51+
return {
52+
localEnabled: typeof semantic.localEnabled === "boolean" ? semantic.localEnabled : defaults.localEnabled,
53+
mcpSamplingEnabled: typeof semantic.mcpSamplingEnabled === "boolean" ? semantic.mcpSamplingEnabled : defaults.mcpSamplingEnabled,
54+
baseUrl: typeof semantic.baseUrl === "string" && semantic.baseUrl.trim() ? semantic.baseUrl.trim() : defaults.baseUrl,
55+
models: Array.isArray(semantic.models) && semantic.models.length > 0 && semantic.models.every(model => typeof model === "string" && model.trim())
56+
? semantic.models.map(model => model.trim())
57+
: defaults.models,
58+
timeoutMs: typeof semantic.timeoutMs === "number" && Number.isFinite(semantic.timeoutMs) && semantic.timeoutMs > 0
59+
? semantic.timeoutMs
60+
: defaults.timeoutMs,
61+
temperature: typeof semantic.temperature === "number" && Number.isFinite(semantic.temperature) && semantic.temperature >= 0 && semantic.temperature <= 2
62+
? semantic.temperature
63+
: defaults.temperature,
64+
allowNonLoopback: typeof semantic.allowNonLoopback === "boolean" ? semantic.allowNonLoopback : defaults.allowNonLoopback,
65+
};
66+
}
67+
2868
static getPredictiveMandates(): string[] {
2969
const logs = AgenticBlackboard.getLogs();
3070
const recent = logs.slice(0, 10).map(l => l.message.toLowerCase());

universal-refiner/src/core/dashboard.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ <h2>Autonomous Prompt Templates</h2>
227227
<span class="badge confidence-${l.confidence}">${l.confidence.toUpperCase()} CONFIDENCE</span>
228228
</div>
229229
<p style="margin: 10px 0; font-size: 0.9rem;">${escapeHtml(l.summary)}</p>
230-
<div style="font-size: 0.7rem; color: var(--dim);">TYPE: ${l.lesson_type.toUpperCase()} • SOURCE: ${l.source}</div>
230+
<div style="font-size: 0.7rem; color: var(--dim);">TYPE: ${l.lesson_type.toUpperCase()} • SOURCE: ${l.source} • REVIEW: ${l.approved === 1 ? 'APPROVED' : l.approved === -1 ? 'REJECTED' : 'PENDING'}</div>
231231
</div>
232232
`).join('') || '<p style="color:var(--dim)">No lessons extracted yet. Run derive_lessons!</p>';
233233
}
@@ -237,10 +237,10 @@ <h2>Autonomous Prompt Templates</h2>
237237
const data = await res.json();
238238
document.getElementById('template-list').innerHTML = data.map(t => `
239239
<div class="card" style="border-top: 2px solid var(--accent);">
240-
<strong>${escapeHtml(t.name)}</strong>
241-
<p style="color:var(--dim); font-size:0.8rem;">${escapeHtml(t.description)}</p>
240+
<strong>${escapeHtml(t.title)}</strong>
241+
<p style="color:var(--dim); font-size:0.8rem;">${escapeHtml(t.usage_notes)}</p>
242242
<pre>${escapeHtml(t.template_text)}</pre>
243-
<div style="margin-top:10px; font-size:0.7rem;">SUCCESS SCORE: ${t.success_score}%</div>
243+
<div style="margin-top:10px; font-size:0.7rem;">SUCCESS SCORE: ${t.success_score}% • REVIEW: ${t.approved === 1 ? 'APPROVED' : t.deprecated === 1 ? 'REJECTED' : 'PENDING'}</div>
244244
</div>
245245
`).join('') || '<p style="color:var(--dim)">Prompt library is currently empty.</p>';
246246
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { RuntimeLogger } from "./logger.js";
2+
3+
export interface SemanticRequest {
4+
taskName: string;
5+
prompt: string;
6+
maxTokens: number;
7+
}
8+
9+
export interface SemanticResponse {
10+
text: string;
11+
provider: string;
12+
model: string;
13+
latencyMs: number;
14+
promptTokens?: number;
15+
completionTokens?: number;
16+
fallbackFrom?: string[];
17+
}
18+
19+
export interface SemanticProvider {
20+
readonly name: string;
21+
requestText(request: SemanticRequest): Promise<SemanticResponse | null>;
22+
}
23+
24+
export interface LocalOpenAiProviderOptions {
25+
baseUrl: string;
26+
models: string[];
27+
timeoutMs: number;
28+
temperature: number;
29+
allowNonLoopback: boolean;
30+
}
31+
32+
function isLoopbackUrl(rawUrl: string): boolean {
33+
try {
34+
const hostname = new URL(rawUrl).hostname.toLowerCase();
35+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
36+
} catch {
37+
return false;
38+
}
39+
}
40+
41+
function getErrorMessage(error: unknown): string {
42+
return error instanceof Error ? error.message : String(error);
43+
}
44+
45+
export class LocalOpenAiProvider implements SemanticProvider {
46+
readonly name = "local-openai";
47+
private readonly options: LocalOpenAiProviderOptions;
48+
49+
constructor(options: LocalOpenAiProviderOptions) {
50+
if (!options.allowNonLoopback && !isLoopbackUrl(options.baseUrl)) {
51+
throw new Error("Local semantic provider base URL must use a loopback host unless allowNonLoopback is enabled.");
52+
}
53+
this.options = options;
54+
}
55+
56+
async requestText(request: SemanticRequest): Promise<SemanticResponse | null> {
57+
const failedModels: string[] = [];
58+
for (const model of this.options.models) {
59+
const startedAt = Date.now();
60+
try {
61+
const response = await fetch(`${this.options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
62+
method: "POST",
63+
headers: { "content-type": "application/json" },
64+
body: JSON.stringify({
65+
model,
66+
messages: [{ role: "user", content: request.prompt }],
67+
stream: false,
68+
temperature: this.options.temperature,
69+
max_tokens: request.maxTokens,
70+
}),
71+
signal: AbortSignal.timeout(this.options.timeoutMs),
72+
});
73+
74+
if (!response.ok) {
75+
throw new Error(`HTTP ${response.status}`);
76+
}
77+
78+
const payload = await response.json() as {
79+
choices?: Array<{ message?: { content?: unknown } }>;
80+
usage?: { prompt_tokens?: number; completion_tokens?: number };
81+
};
82+
const text = payload.choices?.[0]?.message?.content;
83+
if (typeof text !== "string" || text.trim().length === 0) {
84+
throw new Error("Response did not contain assistant text.");
85+
}
86+
87+
return {
88+
text,
89+
provider: this.name,
90+
model,
91+
latencyMs: Date.now() - startedAt,
92+
promptTokens: payload.usage?.prompt_tokens,
93+
completionTokens: payload.usage?.completion_tokens,
94+
fallbackFrom: failedModels,
95+
};
96+
} catch (error) {
97+
failedModels.push(model);
98+
RuntimeLogger.warn(`${request.taskName} local model attempt failed`, {
99+
provider: this.name,
100+
model,
101+
latencyMs: Date.now() - startedAt,
102+
error: getErrorMessage(error),
103+
});
104+
}
105+
}
106+
107+
return null;
108+
}
109+
}
110+
111+
export class McpSamplingProvider implements SemanticProvider {
112+
readonly name = "mcp-sampling";
113+
private readonly sample: (prompt: string, maxTokens: number) => Promise<string | null>;
114+
115+
constructor(sample: (prompt: string, maxTokens: number) => Promise<string | null>) {
116+
this.sample = sample;
117+
}
118+
119+
async requestText(request: SemanticRequest): Promise<SemanticResponse | null> {
120+
const startedAt = Date.now();
121+
const text = await this.sample(request.prompt, request.maxTokens);
122+
if (!text) {
123+
return null;
124+
}
125+
126+
return {
127+
text,
128+
provider: this.name,
129+
model: "client-selected",
130+
latencyMs: Date.now() - startedAt,
131+
};
132+
}
133+
}
134+
135+
export class SemanticProviderChain {
136+
private readonly providers: SemanticProvider[];
137+
private readonly onSuccess?: (response: SemanticResponse, request: SemanticRequest) => void;
138+
139+
constructor(
140+
providers: SemanticProvider[],
141+
onSuccess?: (response: SemanticResponse, request: SemanticRequest) => void,
142+
) {
143+
this.providers = providers;
144+
this.onSuccess = onSuccess;
145+
}
146+
147+
async requestText(request: SemanticRequest): Promise<string | null> {
148+
const unavailableProviders: string[] = [];
149+
for (const provider of this.providers) {
150+
const response = await provider.requestText(request);
151+
if (response) {
152+
response.fallbackFrom = [
153+
...unavailableProviders.map(name => `provider:${name}`),
154+
...(response.fallbackFrom || []).map(model => `model:${model}`),
155+
];
156+
this.onSuccess?.(response, request);
157+
return response.text;
158+
}
159+
unavailableProviders.push(provider.name);
160+
}
161+
162+
RuntimeLogger.warn(`${request.taskName} exhausted all semantic providers`);
163+
return null;
164+
}
165+
}

0 commit comments

Comments
 (0)