mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 10:43:46 +00:00
Merge remote-tracking branch 'origin/feat/ifcchat-claude' into v0.8.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
IfcOpenShell AI Assistant
|
IfcOpenShell AI Assistant
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
A web-based client-side (pyodide + OpenAI API) model interrogation and generation API based on: ifcedit, ifcquery and ifcmcp (ifcopenshell-mcp) packaged in a HTML+JS application.
|
A web-based client-side (pyodide + OpenAI, Anthropic, or OpenRouter API) model interrogation and generation API based on: ifcedit, ifcquery and ifcmcp (ifcopenshell-mcp) packaged in a HTML+JS application.
|
||||||
|
|
||||||
### Setup instructions
|
### Setup instructions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// This file was generated with the assistance of an AI coding tool.
|
||||||
|
|
||||||
|
function parseArguments(argumentsText) {
|
||||||
|
if (!argumentsText) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(argumentsText);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnthropicTools(tools = []) {
|
||||||
|
return tools.map((tool) => ({
|
||||||
|
name: tool.function.name,
|
||||||
|
description: tool.function.description,
|
||||||
|
input_schema: tool.function.parameters,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnthropicAssistantContent(message) {
|
||||||
|
const content = [];
|
||||||
|
|
||||||
|
if (message.content) {
|
||||||
|
content.push({ type: "text", text: message.content });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const toolCall of message.tool_calls ?? []) {
|
||||||
|
content.push({
|
||||||
|
type: "tool_use",
|
||||||
|
id: toolCall.id,
|
||||||
|
name: toolCall.function.name,
|
||||||
|
input: parseArguments(toolCall.function.arguments),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.length === 1 && content[0].type === "text" ? content[0].text : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnthropicUserContent(message) {
|
||||||
|
return typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAnthropicToolResult(message) {
|
||||||
|
return {
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: message.tool_call_id,
|
||||||
|
content: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitSystemAndMessages(messages = []) {
|
||||||
|
const system = [];
|
||||||
|
const anthropicMessages = [];
|
||||||
|
let pendingToolResults = [];
|
||||||
|
|
||||||
|
const flushToolResults = () => {
|
||||||
|
if (pendingToolResults.length === 0) return;
|
||||||
|
anthropicMessages.push({ role: "user", content: pendingToolResults });
|
||||||
|
pendingToolResults = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "system") {
|
||||||
|
if (message.content) {
|
||||||
|
system.push(message.content);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === "tool") {
|
||||||
|
pendingToolResults.push(toAnthropicToolResult(message));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushToolResults();
|
||||||
|
|
||||||
|
if (message.role === "user") {
|
||||||
|
anthropicMessages.push({
|
||||||
|
role: "user",
|
||||||
|
content: toAnthropicUserContent(message),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
anthropicMessages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: toAnthropicAssistantContent(message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushToolResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
system: system.join("\n\n"),
|
||||||
|
messages: anthropicMessages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toChatCompletionResponse(response) {
|
||||||
|
const text = [];
|
||||||
|
const toolCalls = [];
|
||||||
|
|
||||||
|
for (const block of response.content ?? []) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
text.push(block.text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.type === "tool_use") {
|
||||||
|
toolCalls.push({
|
||||||
|
id: block.id,
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: block.name,
|
||||||
|
arguments: JSON.stringify(block.input ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = { role: "assistant" };
|
||||||
|
const content = text.join("\n").trim();
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
message.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolCalls.length) {
|
||||||
|
message.tool_calls = toolCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function chat({ apiKey, model, messages, tools }) {
|
||||||
|
const request = splitSystemAndMessages(messages);
|
||||||
|
const body = {
|
||||||
|
model,
|
||||||
|
max_tokens: 4096,
|
||||||
|
messages: request.messages,
|
||||||
|
tools: toAnthropicTools(tools),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.system) {
|
||||||
|
body.system = request.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`Anthropic error ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toChatCompletionResponse(await res.json());
|
||||||
|
}
|
||||||
+29
-2
@@ -1,10 +1,13 @@
|
|||||||
// app.js
|
// app.js
|
||||||
import * as openaiApi from "./api_openai.js";
|
import * as openaiApi from "./api_openai.js";
|
||||||
|
import * as anthropicApi from "./api_anthropic.js";
|
||||||
import * as openrouterApi from "./api_openrouter.js";
|
import * as openrouterApi from "./api_openrouter.js";
|
||||||
|
|
||||||
const PROVIDERS = {
|
const PROVIDERS = {
|
||||||
openai: {
|
openai: {
|
||||||
api: openaiApi,
|
api: openaiApi,
|
||||||
|
apiKeyLabel: "OpenAI API key",
|
||||||
|
apiKeyPlaceholder: "sk-...",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
value: "gpt-5",
|
value: "gpt-5",
|
||||||
@@ -16,8 +19,29 @@ const PROVIDERS = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
anthropic: {
|
||||||
|
api: anthropicApi,
|
||||||
|
apiKeyLabel: "Anthropic API key",
|
||||||
|
apiKeyPlaceholder: "sk-ant-...",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
value: "claude-sonnet-4-6",
|
||||||
|
label: "claude-sonnet-4-6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "claude-opus-4-6",
|
||||||
|
label: "claude-opus-4-6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "claude-haiku-4-5-20251001",
|
||||||
|
label: "claude-haiku-4-5"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
api: openrouterApi,
|
api: openrouterApi,
|
||||||
|
apiKeyLabel: "OpenRouter API key",
|
||||||
|
apiKeyPlaceholder: "sk-or-v1-...",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
value: "openai/gpt-oss-20b",
|
value: "openai/gpt-oss-20b",
|
||||||
@@ -54,6 +78,7 @@ const msgsEl = $("msgs");
|
|||||||
const sendBtn = $("send");
|
const sendBtn = $("send");
|
||||||
const inputEl = $("input");
|
const inputEl = $("input");
|
||||||
const apiKeyEl = $("apiKey");
|
const apiKeyEl = $("apiKey");
|
||||||
|
const apiKeyLabelEl = $("apiKeyLabel");
|
||||||
const modelEl = $("model");
|
const modelEl = $("model");
|
||||||
const providerEl = $("provider");
|
const providerEl = $("provider");
|
||||||
const ifcFileEl = $("ifcFile");
|
const ifcFileEl = $("ifcFile");
|
||||||
@@ -61,8 +86,10 @@ const newBtn = $("newModel");
|
|||||||
const downloadBtn = $("downloadIfc");
|
const downloadBtn = $("downloadIfc");
|
||||||
|
|
||||||
function onProviderChange() {
|
function onProviderChange() {
|
||||||
const p = PROVIDERS[providerEl.value];
|
const provider = PROVIDERS[providerEl.value];
|
||||||
modelEl.innerHTML = p.models.map(m => `<option value="${m.value}">${m.label}</option>`).join("");
|
apiKeyLabelEl.innerHTML = `${provider.apiKeyLabel}<span class="small">stored in browser memory; only sent to provider servers</span>`;
|
||||||
|
apiKeyEl.placeholder = provider.apiKeyPlaceholder;
|
||||||
|
modelEl.innerHTML = provider.models.map(m => `<option value="${m.value}">${m.label}</option>`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
providerEl.addEventListener("change", onProviderChange);
|
providerEl.addEventListener("change", onProviderChange);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<label>Provider</label>
|
<label>Provider</label>
|
||||||
<select id="provider" style="width: 100%;">
|
<select id="provider" style="width: 100%;">
|
||||||
<option value="openai">OpenAI</option>
|
<option value="openai">OpenAI</option>
|
||||||
|
<option value="anthropic">Anthropic (Claude)</option>
|
||||||
<option value="openrouter">OpenRouter</option>
|
<option value="openrouter">OpenRouter</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,14 +27,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>API key<span class="small">stored in browser memory</span></label>
|
<label id="apiKeyLabel">API key<span class="small">stored in browser memory; only sent to provider servers</span></label>
|
||||||
<input id="apiKey" type="password" placeholder="sk-..." autocomplete="off" style="width: 100%;" />
|
<input id="apiKey" type="password" placeholder="sk-..." autocomplete="off" style="width: 100%;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="row" style="margin-bottom: 10px;">
|
<div class="row" style="margin-bottom: 10px;">
|
||||||
<label>IFC model context<span class="small">stored in browser memory; only information extracted through MCP commands sent to OpenAI servers</span></label>
|
<label>IFC model context<span class="small">stored in browser memory; only information extracted through MCP commands sent to provider servers</span></label>
|
||||||
|
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<label class="btn" id="browseBtn" for="ifcFile" role="button" tabindex="0">
|
<label class="btn" id="browseBtn" for="ifcFile" role="button" tabindex="0">
|
||||||
|
|||||||
Reference in New Issue
Block a user