Skip to content

Commit d37be68

Browse files
committed
Merge branch 'main' into sfierro/wk-tui
2 parents b1d9efe + 084b795 commit d37be68

27 files changed

Lines changed: 476 additions & 56 deletions

File tree

app/desktop/studio_server/prompt_api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
class PromptApiResponse(BaseModel):
1212
prompt: str
1313
prompt_id: PromptId
14+
chain_of_thought_instructions: str | None = None
1415

1516

1617
def connect_prompt_api(app: FastAPI):
@@ -35,11 +36,15 @@ async def generate_prompt(
3536

3637
try:
3738
prompt_builder = prompt_builder_from_id(prompt_id, task)
38-
prompt = prompt_builder.build_prompt_for_ui()
39+
# Return the base prompt without thinking instructions appended so
40+
# the UI can render the chain of thought as a separate, editable field.
41+
prompt = prompt_builder.build_prompt(include_json_instructions=False)
42+
cot_prompt = prompt_builder.chain_of_thought_prompt()
3943
except Exception as e:
4044
raise HTTPException(status_code=400, detail=str(e))
4145

4246
return PromptApiResponse(
4347
prompt=prompt,
4448
prompt_id=prompt_id,
49+
chain_of_thought_instructions=cot_prompt,
4550
)

app/desktop/studio_server/skill_api.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
from fastapi import FastAPI, HTTPException, Path
66
from kiln_ai.datamodel.skill import Skill
7+
from kiln_ai.utils.filesystem import open_folder
78
from kiln_ai.utils.validation import SkillNameString
9+
from kiln_server.document_api import OpenFileResponse
810
from kiln_server.project_api import project_from_id
911
from kiln_server.utils.agent_checks.policy import (
1012
ALLOW_AGENT,
13+
DENY_AGENT,
1114
agent_policy_require_approval,
1215
)
1316
from pydantic import BaseModel, Field
@@ -176,3 +179,25 @@ async def update_skill(
176179
updated.save_to_file()
177180

178181
return skill_to_response(updated)
182+
183+
@app.post(
184+
"/api/projects/{project_id}/skills/{skill_id}/open_enclosing_folder",
185+
tags=["Skills"],
186+
openapi_extra=DENY_AGENT,
187+
)
188+
async def open_skill_enclosing_folder(
189+
project_id: Annotated[
190+
str, Path(description="The unique identifier of the project.")
191+
],
192+
skill_id: Annotated[
193+
str, Path(description="The unique identifier of the skill.")
194+
],
195+
) -> OpenFileResponse:
196+
skill = _get_skill(project_id, skill_id)
197+
if not skill.path:
198+
raise HTTPException(
199+
status_code=500,
200+
detail="Skill path not found",
201+
)
202+
open_folder(skill.path)
203+
return OpenFileResponse(path=str(skill.path.parent))

app/desktop/studio_server/test_prompt_api.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ def build_base_prompt(self):
2828
def build_prompt_for_ui(self):
2929
return "Mock prompt for UI"
3030

31+
def build_prompt(self, include_json_instructions=False, skills=None):
32+
return "Mock prompt"
33+
34+
def chain_of_thought_prompt(self):
35+
return None
36+
3137

3238
@pytest.fixture
3339
def mock_task():
@@ -58,8 +64,9 @@ def test_generate_prompt_success(
5864
assert response.status_code == 200
5965
data = response.json()
6066
assert data == {
61-
"prompt": "Mock prompt for UI",
67+
"prompt": "Mock prompt",
6268
"prompt_id": "simple_prompt_builder",
69+
"chain_of_thought_instructions": None,
6370
}
6471

6572
mock_task_from_id.assert_called_once_with("project123", "task456")

app/web_ui/src/lib/api_schema.d.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,6 +2402,23 @@ export interface paths {
24022402
patch?: never;
24032403
trace?: never;
24042404
};
2405+
"/api/projects/{project_id}/skills/{skill_id}/open_enclosing_folder": {
2406+
parameters: {
2407+
query?: never;
2408+
header?: never;
2409+
path?: never;
2410+
cookie?: never;
2411+
};
2412+
get?: never;
2413+
put?: never;
2414+
/** Open Skill Enclosing Folder */
2415+
post: operations["open_skill_enclosing_folder_api_projects__project_id__skills__skill_id__open_enclosing_folder_post"];
2416+
delete?: never;
2417+
options?: never;
2418+
head?: never;
2419+
patch?: never;
2420+
trace?: never;
2421+
};
24052422
"/api/projects/{project_id}/tasks/{task_id}/prompt_optimization_jobs/check_run_config": {
24062423
parameters: {
24072424
query?: never;
@@ -7400,6 +7417,8 @@ export interface components {
74007417
prompt: string;
74017418
/** Prompt Id */
74027419
prompt_id: string;
7420+
/** Chain Of Thought Instructions */
7421+
chain_of_thought_instructions?: string | null;
74037422
};
74047423
/**
74057424
* PromptCreateRequest
@@ -15813,6 +15832,40 @@ export interface operations {
1581315832
};
1581415833
};
1581515834
};
15835+
open_skill_enclosing_folder_api_projects__project_id__skills__skill_id__open_enclosing_folder_post: {
15836+
parameters: {
15837+
query?: never;
15838+
header?: never;
15839+
path: {
15840+
/** @description The unique identifier of the project. */
15841+
project_id: string;
15842+
/** @description The unique identifier of the skill. */
15843+
skill_id: string;
15844+
};
15845+
cookie?: never;
15846+
};
15847+
requestBody?: never;
15848+
responses: {
15849+
/** @description Successful Response */
15850+
200: {
15851+
headers: {
15852+
[name: string]: unknown;
15853+
};
15854+
content: {
15855+
"application/json": components["schemas"]["OpenFileResponse"];
15856+
};
15857+
};
15858+
/** @description Validation Error */
15859+
422: {
15860+
headers: {
15861+
[name: string]: unknown;
15862+
};
15863+
content: {
15864+
"application/json": components["schemas"]["HTTPValidationError"];
15865+
};
15866+
};
15867+
};
15868+
};
1581615869
check_run_config_api_projects__project_id__tasks__task_id__prompt_optimization_jobs_check_run_config_get: {
1581715870
parameters: {
1581815871
query: {

app/web_ui/src/routes/(app)/prompts/[project_id]/[task_id]/prompt_generators/prompt_generators.ts renamed to app/web_ui/src/lib/prompt_generators.ts

File renamed without changes.

app/web_ui/src/lib/ui/fancy_select.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,10 @@
695695
<button
696696
type="button"
697697
class="btn btn-xs btn-primary btn-outline rounded-full"
698-
on:click={option.action_handler}
698+
on:mousedown|preventDefault|stopPropagation
699+
on:click|preventDefault|stopPropagation={() => {
700+
option.action_handler?.()
701+
}}
699702
>
700703
{option.action_label}
701704
</button>

app/web_ui/src/lib/ui/run_config_component/prompt_type_selector.svelte

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import type { PromptResponse } from "$lib/types"
55
import Warning from "$lib/ui/warning.svelte"
66
import type { OptionGroup, Option } from "$lib/ui/fancy_select_types"
7-
import { getStaticPromptDisplayName } from "$lib/utils/run_config_formatters"
7+
import { client } from "$lib/api_client"
8+
import { goto } from "$app/navigation"
9+
import { page } from "$app/stores"
10+
import { prompt_generator_categories } from "$lib/prompt_generators"
811
912
export let prompt_method: string
1013
export let linked_model_selection: string | null | undefined = undefined
@@ -14,19 +17,128 @@
1417
export let fine_tune_prompt_id: string | undefined = undefined
1518
export let description: string | undefined = undefined
1619
export let info_description: string | undefined = undefined
20+
export let project_id: string | null = null
21+
export let task_id: string | null = null
22+
23+
let has_rated_data = false
24+
let has_repair_data = false
25+
let data_requirements_checked = false
26+
27+
$: generator_requirements = build_generator_requirements()
28+
29+
function build_generator_requirements(): Record<
30+
string,
31+
{ requires_data: boolean; requires_repairs: boolean }
32+
> {
33+
const map: Record<
34+
string,
35+
{ requires_data: boolean; requires_repairs: boolean }
36+
> = {}
37+
for (const category of prompt_generator_categories) {
38+
for (const template of category.templates) {
39+
if (template.generator_id) {
40+
map[template.generator_id] = {
41+
requires_data: template.requires_data,
42+
requires_repairs: template.requires_repairs,
43+
}
44+
}
45+
}
46+
}
47+
return map
48+
}
49+
50+
function generator_disabled_reason(generator_id: string): string | null {
51+
const req = generator_requirements[generator_id]
52+
if (!req || !data_requirements_checked) {
53+
return null
54+
}
55+
if (req.requires_repairs && !has_repair_data) {
56+
return "Requires at least one repaired response in your dataset."
57+
}
58+
if (req.requires_data && !has_rated_data) {
59+
return "Requires at least one rated response in your dataset."
60+
}
61+
return null
62+
}
63+
64+
// Re-fetch rated/repair data whenever project_id or task_id changes so the
65+
// generator disabled states stay in sync if the parent swaps tasks.
66+
let requirements_loaded_key: string | null = null
67+
$: load_data_requirements(project_id, task_id)
68+
69+
async function load_data_requirements(
70+
project_id: string | null,
71+
task_id: string | null,
72+
) {
73+
if (!project_id || !task_id) {
74+
requirements_loaded_key = null
75+
data_requirements_checked = false
76+
has_rated_data = false
77+
has_repair_data = false
78+
return
79+
}
80+
const key = `${project_id}:${task_id}`
81+
if (requirements_loaded_key === key) return
82+
requirements_loaded_key = key
83+
data_requirements_checked = false
84+
has_rated_data = false
85+
has_repair_data = false
86+
try {
87+
const { data, error } = await client.GET(
88+
"/api/projects/{project_id}/tasks/{task_id}/runs_summaries",
89+
{
90+
params: {
91+
path: { project_id, task_id },
92+
},
93+
},
94+
)
95+
// Drop stale responses if the task was swapped while the request was
96+
// in flight — avoids overwriting the new task's flags with old data.
97+
if (requirements_loaded_key !== key) return
98+
if (error) return
99+
if (data) {
100+
has_rated_data = data.some(
101+
(run) =>
102+
run.rating &&
103+
run.rating.value !== null &&
104+
run.rating.value !== undefined,
105+
)
106+
has_repair_data = data.some(
107+
(run) => run.repair_state?.toLowerCase() === "repaired",
108+
)
109+
}
110+
} finally {
111+
if (requirements_loaded_key === key) {
112+
data_requirements_checked = true
113+
}
114+
}
115+
}
17116
18117
$: options = build_prompt_options(
19118
$current_task_prompts,
20119
exclude_cot,
21120
custom_prompt_name,
22121
fine_tune_prompt_id,
122+
project_id,
123+
task_id,
124+
data_requirements_checked,
125+
has_rated_data,
126+
has_repair_data,
23127
)
24128
25129
function build_prompt_options(
26130
current_task_prompts: PromptResponse | null,
27131
exclude_cot: boolean,
28132
custom_prompt_name: string | undefined,
29133
fine_tune_prompt_id: string | undefined,
134+
project_id: string | null,
135+
task_id: string | null,
136+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
137+
_requirements_checked: boolean,
138+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
139+
_has_rated: boolean,
140+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
141+
_has_repair: boolean,
30142
): OptionGroup[] {
31143
if (!current_task_prompts) {
32144
return []
@@ -39,10 +151,12 @@
39151
if (generator.chain_of_thought && exclude_cot) {
40152
continue
41153
}
154+
const disabled_reason = generator_disabled_reason(generator.id)
42155
generators.push({
43156
value: generator.id,
44157
label: generator.name,
45-
description: generator.short_description,
158+
description: disabled_reason ?? generator.short_description,
159+
disabled: !!disabled_reason,
46160
})
47161
}
48162
if (generators.length > 0) {
@@ -84,17 +198,39 @@
84198
}
85199
static_prompts.push({
86200
value: prompt.id,
87-
label: getStaticPromptDisplayName(
88-
prompt.name,
89-
prompt.generator_id,
90-
current_task_prompts,
91-
),
201+
label: prompt.name,
92202
})
93203
}
204+
const saved_prompts_action =
205+
project_id && task_id
206+
? {
207+
action_label: "Create New",
208+
action_handler: () => {
209+
const params = new URLSearchParams()
210+
const from = $page.url.searchParams.get("from")
211+
if (from) {
212+
params.set("from", from)
213+
}
214+
const qs = params.toString()
215+
goto(
216+
`/prompts/${project_id}/${task_id}/prompt_generators${
217+
qs ? `?${qs}` : ""
218+
}`,
219+
)
220+
},
221+
}
222+
: {}
94223
if (static_prompts.length > 0) {
95224
grouped_options.push({
96225
label: "Saved Prompts",
97226
options: static_prompts,
227+
...saved_prompts_action,
228+
})
229+
} else if (project_id && task_id) {
230+
grouped_options.push({
231+
label: "Saved Prompts",
232+
options: [],
233+
...saved_prompts_action,
98234
})
99235
}
100236
return grouped_options
@@ -128,6 +264,11 @@
128264
exclude_cot,
129265
custom_prompt_name,
130266
fine_tune_prompt_id,
267+
project_id,
268+
task_id,
269+
data_requirements_checked,
270+
has_rated_data,
271+
has_repair_data,
131272
)
132273
}
133274
</script>

0 commit comments

Comments
 (0)