Skip to content
Merged
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
16 changes: 11 additions & 5 deletions app/desktop/studio_server/provider_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,11 +1140,17 @@ async def connect_bedrock(key_data: dict):

async def connect_kiln_copilot(key: str):
base_url = os.environ.get("KILN_SERVER_BASE_URL", "https://api.kiln.tech")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Irrelevant to this PR, but we should make this part of the settings / config (and try to eventually centralize env vars over there)

async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/v1/verify_api_key",
headers={"Authorization": f"Bearer {key}"},
timeout=20,
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/v1/verify_api_key",
headers={"Authorization": f"Bearer {key}"},
timeout=20,
)
except httpx.RequestError as e:
return JSONResponse(
status_code=400,
content={"message": f"Failed to connect to Kiln Copilot. Error: {e!s}"},
)

if response.status_code == 200:
Expand Down
29 changes: 29 additions & 0 deletions app/desktop/studio_server/test_provider_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,35 @@ def test_connect_api_key_kiln_copilot_empty_key(client):
assert response.json() == {"detail": "API Key not found"}


@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
def test_connect_api_key_kiln_copilot_failure(mock_httpx_get, client):
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.json.return_value = {"message": "Invalid API key"}
mock_httpx_get.return_value = mock_response

response = client.post(
"/api/provider/connect_api_key",
json={"provider": "kiln_copilot", "key_data": {"API Key": "invalid_key"}},
)

assert response.status_code == 401
assert response.json() == {"message": "Invalid API key"}


@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
def test_connect_api_key_kiln_copilot_network_error(mock_httpx_get, client):
mock_httpx_get.side_effect = httpx.RequestError("Network error")

response = client.post(
"/api/provider/connect_api_key",
json={"provider": "kiln_copilot", "key_data": {"API Key": "test_key"}},
)

assert response.status_code == 400
assert "Failed to connect" in response.json()["message"]


@patch("app.desktop.studio_server.provider_api.requests.get")
@patch("app.desktop.studio_server.provider_api.Config.shared")
def test_connect_openai_success(mock_config_shared, mock_requests_get, client):
Expand Down
7 changes: 4 additions & 3 deletions app/web_ui/src/lib/ui/dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
disabled?: boolean
loading?: boolean
hide?: boolean
width?: "normal" | "wide"
}
export let action_buttons: ActionButton[] = []
let action_running = false
Expand Down Expand Up @@ -187,9 +188,9 @@
</form>
{:else}
<button
class="btn btn-sm h-10 min-w-24 {button.isPrimary
? 'btn-primary'
: ''}
class="btn btn-sm h-10 min-w-24 {button.width === 'wide'
? 'w-full'
: ''} {button.isPrimary ? 'btn-primary' : ''}
{button.isError ? 'btn-error' : ''}
{button.isWarning ? 'btn-warning' : ''}"
disabled={button.disabled || button.loading}
Expand Down
206 changes: 206 additions & 0 deletions app/web_ui/src/lib/ui/kiln_copilot/connect_kiln_copilot_steps.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<script lang="ts">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is all extracted out from app/web_ui/src/routes/(fullscreen)/setup/(setup)/connect_providers/kiln_copilot/connect_kiln_copilot.svelte

import { onMount } from "svelte"
import createKindeClient from "@kinde-oss/kinde-auth-pkce-js"
import { base_url } from "$lib/api_client"
import posthog from "posthog-js"
import { env } from "$env/dynamic/public"
export let onSuccess: () => void
export let showTitle = true
export let showCheckmark = false
let kindeClient: Awaited<ReturnType<typeof createKindeClient>> | null = null
let apiKey = ""
let apiKeyError = false
let apiKeyMessage: string | null = null
let submitting = false
const KINDE_ACCOUNT_DOMAIN =
env.PUBLIC_KINDE_ACCOUNT_DOMAIN || "https://account.kiln.tech"
const KINDE_ACCOUNT_CLIENT_ID =
env.PUBLIC_KINDE_ACCOUNT_CLIENT_ID || "2428f47a1e0b404b82e68400a2d580c6"
async function initKindeClient() {
if (kindeClient) return kindeClient
kindeClient = await createKindeClient({
client_id: KINDE_ACCOUNT_CLIENT_ID,
domain: KINDE_ACCOUNT_DOMAIN,
redirect_uri: window.location.origin + window.location.pathname,
on_redirect_callback: () => {},
})
return kindeClient
}
async function openSignup() {
try {
const kinde = await initKindeClient()
if (!kinde) {
apiKeyError = true
apiKeyMessage = "Kinde configuration is missing"
return
}
await kinde.register()
} catch (e) {
console.error("openSignup error", e)
apiKeyError = true
apiKeyMessage = "Failed to open Kiln Copilot signup"
}
}
async function openSelfServePortal() {
try {
const kinde = await initKindeClient()
if (!kinde) {
apiKeyError = true
apiKeyMessage = "Please sign up first"
return
}
const accessToken = await kinde.getToken()
if (!accessToken) {
apiKeyError = true
apiKeyMessage = "Please sign up first before accessing the portal"
return
}
const response = await fetch(
`${KINDE_ACCOUNT_DOMAIN}/account_api/v1/portal_link`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
)
if (!response.ok) {
throw new Error("Failed to generate portal link")
}
const data = await response.json()
window.open(data.url, "_blank")
} catch (e) {
console.error("openSelfServePortal error", e)
apiKeyError = true
apiKeyMessage =
"Failed to open self-serve portal. Please try signing up first."
}
}
export async function submitApiKey() {
if (!apiKey.trim()) {
apiKeyError = true
return false
}
apiKeyError = false
apiKeyMessage = null
submitting = true
try {
const res = await fetch(base_url + "/api/provider/connect_api_key", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
provider: "kiln_copilot",
key_data: {
"API Key": apiKey,
},
}),
})
const data = await res.json()
if (!res.ok) {
apiKeyMessage =
data.message || data.detail || "Failed to connect to provider"
apiKeyError = true
return false
}
posthog.capture("connect_provider", {
provider_id: "kiln_copilot",
})
onSuccess()
return true
} catch (e) {
console.error("submitApiKey error", e)
apiKeyMessage = "Failed to connect to provider (Exception: " + e + ")"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we could now use your server error message now and throw from inside the try and turn it into a KilnError like we usually do for failing API calls. Probably not in scope of this PR though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created KIL-354 for that!

apiKeyError = true
return false
} finally {
submitting = false
}
}
onMount(async () => {
if (
window.location.search.includes("code=") ||
window.location.search.includes("state=")
) {
await initKindeClient()
}
})
</script>

{#if showTitle}
<h1 class="text-xl font-medium text-center mb-2">Connect Kiln Copilot</h1>
{/if}

<ol class="mb-2 text-gray-700">
<li class="list-decimal pl-1 mx-8 mb-4">
<button class="link" on:click={openSignup}>Sign Up</button>
to create your Kiln Copilot account.
</li>
<li class="list-decimal pl-1 mx-8 my-4">
After registration,
<button class="link" on:click={openSelfServePortal}>
open the self-serve portal.
</button>
Go to 'API keys' section and create an API key.
</li>
<li class="list-decimal pl-1 mx-8 my-4">
Copy your API key, paste it below and click 'Connect'.
</li>
</ol>

{#if apiKeyMessage}
<p class="text-error text-center pb-4">{apiKeyMessage}</p>
{/if}

<div class="flex flex-row gap-4 items-center">
<div class="grow flex flex-col gap-2">
<input
type="text"
id="API Key"
placeholder="API Key"
class="input input-bordered w-full max-w-[300px] {apiKeyError
? 'input-error'
: ''}"
bind:value={apiKey}
on:input={() => (apiKeyError = false)}
/>
</div>
<button
class="btn min-w-[130px]"
on:click={submitApiKey}
disabled={submitting}
>
{#if submitting}
<div class="loading loading-spinner loading-md"></div>
{:else if showCheckmark}
<img
src="/images/circle-check.svg"
class="size-6 group-hover:hidden"
alt="Connected"
/>
{:else}
Connect
{/if}
</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</script>

<AppPage
title="Connect Kiln Copilot"
title="Kiln Copilot"
breadcrumbs={[
{ label: "Settings", href: "/settings" },
{ label: "AI Providers", href: "/settings/providers" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
try {
task = await load_task(project_id, task_id)
if (!task) {
throw createKilnError("Task not found")
throw new Error("Task not found")
}
} catch (err) {
task_error = createKilnError(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
try {
task = await load_task(project_id, task_id)
if (!task) {
throw createKilnError("Task not found")
throw new Error("Task not found")
}
} catch (err) {
task_error = createKilnError(err)
Expand Down
Loading
Loading