mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 23:36:20 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
+1
-1
@@ -64,7 +64,7 @@ PYNUMBER:=3$(PYMINOR)
|
|||||||
PYPI_VERSION:=3.$(PYMINOR)
|
PYPI_VERSION:=3.$(PYMINOR)
|
||||||
endif # def PYVERSION
|
endif # def PYVERSION
|
||||||
|
|
||||||
IFCMERGE_VERSION:=2026-04-02
|
IFCMERGE_VERSION:=2026-04-07
|
||||||
|
|
||||||
ifdef PLATFORM
|
ifdef PLATFORM
|
||||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||||
|
|||||||
@@ -151,8 +151,11 @@ def merge_branch(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], operator: bpy.t
|
|||||||
conflicts = ifcgit.git_mergetool(mergetool, path_ifc)
|
conflicts = ifcgit.git_mergetool(mergetool, path_ifc)
|
||||||
if conflicts is not None:
|
if conflicts is not None:
|
||||||
ifcgit.git_merge_abort()
|
ifcgit.git_merge_abort()
|
||||||
ifcgit.store_merge_conflicts(conflicts)
|
if conflicts:
|
||||||
operator.report({"WARNING"}, "Merge failed — see the conflict report in the panel below")
|
ifcgit.store_merge_conflicts(conflicts)
|
||||||
|
operator.report({"WARNING"}, "Merge failed — see the conflict report in the panel below")
|
||||||
|
else:
|
||||||
|
operator.report({"ERROR"}, "Merge tool failed — check that ifcmerge is installed correctly")
|
||||||
return False
|
return False
|
||||||
ifcgit.commit_merge(path_ifc)
|
ifcgit.commit_merge(path_ifc)
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class IfcGit:
|
|||||||
rev=[props.display_branch],
|
rev=[props.display_branch],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
commits_relevant = list(
|
commits_relevant = set(
|
||||||
git.objects.commit.Commit.iter_items(
|
git.objects.commit.Commit.iter_items(
|
||||||
repo=repo,
|
repo=repo,
|
||||||
rev=[props.display_branch],
|
rev=[props.display_branch],
|
||||||
@@ -225,11 +225,17 @@ class IfcGit:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_relevant(commit):
|
||||||
|
if commit in commits_relevant:
|
||||||
|
return True
|
||||||
|
# Merge commits are relevant too
|
||||||
|
return len(commit.parents) > 1 and any(p in commits_relevant for p in commit.parents)
|
||||||
|
|
||||||
for commit in commits:
|
for commit in commits:
|
||||||
|
|
||||||
if props.ifcgit_filter == "tagged" and commit.hexsha not in lookup:
|
if props.ifcgit_filter == "tagged" and commit.hexsha not in lookup:
|
||||||
continue
|
continue
|
||||||
elif props.ifcgit_filter == "relevant" and commit not in commits_relevant:
|
elif props.ifcgit_filter == "relevant" and not is_relevant(commit):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
props.ifcgit_commits.add()
|
props.ifcgit_commits.add()
|
||||||
@@ -239,7 +245,7 @@ class IfcGit:
|
|||||||
list_item.author_name = commit.author.name
|
list_item.author_name = commit.author.name
|
||||||
list_item.author_email = commit.author.email
|
list_item.author_email = commit.author.email
|
||||||
list_item.committed_date = int(commit.committed_date)
|
list_item.committed_date = int(commit.committed_date)
|
||||||
if commit in commits_relevant:
|
if is_relevant(commit):
|
||||||
list_item.relevant = True
|
list_item.relevant = True
|
||||||
if commit.hexsha in lookup:
|
if commit.hexsha in lookup:
|
||||||
for tag in lookup[commit.hexsha]:
|
for tag in lookup[commit.hexsha]:
|
||||||
@@ -589,7 +595,7 @@ class IfcGit:
|
|||||||
"""Attempt a git merge. Returns None on clean merge, 'conflict' on expected
|
"""Attempt a git merge. Returns None on clean merge, 'conflict' on expected
|
||||||
GitCommandError, or 'error' on an unknown GitError."""
|
GitCommandError, or 'error' on an unknown GitError."""
|
||||||
repo = IfcGitRepo.repo
|
repo = IfcGitRepo.repo
|
||||||
branch = repo.branches[branch_name]
|
branch = repo.refs[branch_name]
|
||||||
try:
|
try:
|
||||||
repo.git.merge(branch)
|
repo.git.merge(branch)
|
||||||
return None
|
return None
|
||||||
@@ -603,7 +609,7 @@ class IfcGit:
|
|||||||
"""Attempt a git merge without committing (always leaves a merge state to abort).
|
"""Attempt a git merge without committing (always leaves a merge state to abort).
|
||||||
Returns None on clean merge, 'conflict' on conflict, or 'error' on unknown failure."""
|
Returns None on clean merge, 'conflict' on conflict, or 'error' on unknown failure."""
|
||||||
repo = IfcGitRepo.repo
|
repo = IfcGitRepo.repo
|
||||||
branch = repo.branches[branch_name]
|
branch = repo.refs[branch_name]
|
||||||
try:
|
try:
|
||||||
repo.git.merge(branch, no_commit=True, no_ff=True)
|
repo.git.merge(branch, no_commit=True, no_ff=True)
|
||||||
return None
|
return None
|
||||||
@@ -619,8 +625,8 @@ class IfcGit:
|
|||||||
report_path = path_ifc + ".ifcmerge"
|
report_path = path_ifc + ".ifcmerge"
|
||||||
try:
|
try:
|
||||||
repo.git.mergetool(tool=mergetool)
|
repo.git.mergetool(tool=mergetool)
|
||||||
except git.exc.GitCommandError:
|
except git.exc.GitCommandError as e:
|
||||||
pass
|
print(f"ifcgit: mergetool failed: {e}")
|
||||||
|
|
||||||
conflicts = None
|
conflicts = None
|
||||||
if os.path.exists(report_path):
|
if os.path.exists(report_path):
|
||||||
@@ -636,6 +642,10 @@ class IfcGit:
|
|||||||
os.remove(report_path)
|
os.remove(report_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if conflicts is None and repo.index.unmerged_blobs():
|
||||||
|
conflicts = []
|
||||||
|
|
||||||
return conflicts
|
return conflicts
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+123
-4
@@ -139,6 +139,7 @@ const baseUrlRowEl = $("baseUrlRow");
|
|||||||
const baseUrlLabelEl = $("baseUrlLabel");
|
const baseUrlLabelEl = $("baseUrlLabel");
|
||||||
const baseUrlEl = $("baseUrl");
|
const baseUrlEl = $("baseUrl");
|
||||||
const thinkingIndicatorEl = $("thinkingIndicator");
|
const thinkingIndicatorEl = $("thinkingIndicator");
|
||||||
|
const compactingIndicatorEl = $("compactingIndicator");
|
||||||
const modelEl = $("model");
|
const modelEl = $("model");
|
||||||
const providerEls = document.querySelectorAll('input[name="provider"]');
|
const providerEls = document.querySelectorAll('input[name="provider"]');
|
||||||
const ifcFileEl = $("ifcFile");
|
const ifcFileEl = $("ifcFile");
|
||||||
@@ -395,6 +396,7 @@ function addMessage(role, text) {
|
|||||||
function setStatus(text) {
|
function setStatus(text) {
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
thinkingIndicatorEl.hidden = text !== "Thinking…";
|
thinkingIndicatorEl.hidden = text !== "Thinking…";
|
||||||
|
compactingIndicatorEl.hidden = text !== "Compacting…";
|
||||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +493,7 @@ Rules:
|
|||||||
- If the user asks about model contents (counts, lists, properties, hierarchy), use tools like ifc_summary/ifc_select/ifc_info/ifc_tree.
|
- If the user asks about model contents (counts, lists, properties, hierarchy), use tools like ifc_summary/ifc_select/ifc_info/ifc_tree.
|
||||||
- If the user asks to change the model, prefer: (1) ifc_list to find candidate API modules, (2) ifc_docs for the exact function signature, then (3) ifc_edit.
|
- If the user asks to change the model, prefer: (1) ifc_list to find candidate API modules, (2) ifc_docs for the exact function signature, then (3) ifc_edit.
|
||||||
- If there is no model and the user wants to create one, call ifc_new.
|
- If there is no model and the user wants to create one, call ifc_new.
|
||||||
|
- In case of type errors on api functions, retry providing values as strings (for example in the case of the matrix in geometry.edit_object_placement).
|
||||||
- After edits, explain what changed and suggest downloading the IFC.
|
- After edits, explain what changed and suggest downloading the IFC.
|
||||||
Be concise. Avoid dumping huge trees unless asked.
|
Be concise. Avoid dumping huge trees unless asked.
|
||||||
`;
|
`;
|
||||||
@@ -499,6 +502,11 @@ let messages = []; // running conversation state (Chat Completions style)
|
|||||||
|
|
||||||
const MAX_TOOL_RESULT_CHARS = 0;
|
const MAX_TOOL_RESULT_CHARS = 0;
|
||||||
const MAX_HISTORY_MESSAGES = 40;
|
const MAX_HISTORY_MESSAGES = 40;
|
||||||
|
const ESTIMATED_CHARS_PER_TOKEN = 4;
|
||||||
|
const MAX_ESTIMATED_TOKENS_PER_MINUTE = 24000;
|
||||||
|
const COMPACT_WHEN_ESTIMATED_TOKENS = 18000;
|
||||||
|
const KEEP_RAW_TURN_GROUPS = 1;
|
||||||
|
const minuteTokenMap = new Map();
|
||||||
|
|
||||||
function truncateToolResult(text) {
|
function truncateToolResult(text) {
|
||||||
if (MAX_TOOL_RESULT_CHARS == 0 || text.length <= MAX_TOOL_RESULT_CHARS) return text;
|
if (MAX_TOOL_RESULT_CHARS == 0 || text.length <= MAX_TOOL_RESULT_CHARS) return text;
|
||||||
@@ -518,6 +526,103 @@ function trimHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEstimatedTokenMinuteLog(firstIterationMinuteBucket) {
|
||||||
|
return Array.from(minuteTokenMap.entries())
|
||||||
|
.filter(([minuteBucket]) => minuteBucket >= firstIterationMinuteBucket)
|
||||||
|
.sort(([leftMinuteBucket], [rightMinuteBucket]) => leftMinuteBucket - rightMinuteBucket)
|
||||||
|
.map(([minuteBucket, estimatedTokens]) => ({
|
||||||
|
timestamp: new Date(minuteBucket * 60000).toISOString(),
|
||||||
|
estimated_tokens: estimatedTokens,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chatWithMinuteDelay({ chat, apiKey, baseURL, model, messages, tools }) {
|
||||||
|
const estimatedTokens = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(JSON.stringify({ model, messages, ...(tools ? { tools } : {}) }).length / ESTIMATED_CHARS_PER_TOKEN)
|
||||||
|
);
|
||||||
|
let currentMinuteBucket = Math.floor(Date.now() / 60000);
|
||||||
|
const estimateTokenUsage = (minuteTokenMap.get(currentMinuteBucket) ?? 0) + estimatedTokens;
|
||||||
|
|
||||||
|
if (estimateTokenUsage > MAX_ESTIMATED_TOKENS_PER_MINUTE) {
|
||||||
|
currentMinuteBucket += 1;
|
||||||
|
await new Promise((resolve) => setTimeout(() => resolve(), 60000));
|
||||||
|
}
|
||||||
|
|
||||||
|
minuteTokenMap.set(currentMinuteBucket, (minuteTokenMap.get(currentMinuteBucket) ?? 0) + estimatedTokens);
|
||||||
|
|
||||||
|
return {
|
||||||
|
minuteBucket: currentMinuteBucket,
|
||||||
|
response: await chat({ apiKey, baseURL, model, messages, tools }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compactHistoryWithLLM(chat, apiKey, baseURL, model) {
|
||||||
|
const estimatedTokens = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(JSON.stringify([{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages]).length / ESTIMATED_CHARS_PER_TOKEN)
|
||||||
|
);
|
||||||
|
if (messages.length <= MAX_HISTORY_MESSAGES && estimatedTokens <= COMPACT_WHEN_ESTIMATED_TOKENS) return null;
|
||||||
|
|
||||||
|
const { prefix, groups } = messages.reduce((acc, message) => {
|
||||||
|
if (message.role === "user") {
|
||||||
|
acc.groups.push([message]);
|
||||||
|
} else if (acc.groups.length) {
|
||||||
|
acc.groups[acc.groups.length - 1].push(message);
|
||||||
|
} else {
|
||||||
|
acc.prefix.push(message);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, { prefix: [], groups: [] });
|
||||||
|
|
||||||
|
if (groups.length <= KEEP_RAW_TURN_GROUPS) return null;
|
||||||
|
|
||||||
|
const compacted = [...prefix, ...groups.slice(0, -KEEP_RAW_TURN_GROUPS).flat()];
|
||||||
|
if (!compacted.length) return null;
|
||||||
|
|
||||||
|
setStatus("Compacting…");
|
||||||
|
try {
|
||||||
|
const before = {
|
||||||
|
message_count: messages.length,
|
||||||
|
turn_group_count: groups.length,
|
||||||
|
estimated_tokens: estimatedTokens,
|
||||||
|
};
|
||||||
|
const { minuteBucket, response } = await chatWithMinuteDelay({
|
||||||
|
chat,
|
||||||
|
apiKey,
|
||||||
|
baseURL,
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "Summarize older IFC chat context for continuation. Preserve user goals, model state and schema, edits already applied, important ids, names, selectors, and unresolved questions. Be concise, factual, and use short markdown bullets. Do not mention that this is a summary."
|
||||||
|
},
|
||||||
|
{ role: "user", content: JSON.stringify(compacted) },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const summary = response.choices?.[0]?.message?.content?.trim();
|
||||||
|
|
||||||
|
if (!summary) return minuteBucket;
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{ role: "assistant", content: `[Context summary]\n${summary}` },
|
||||||
|
...groups.slice(-KEEP_RAW_TURN_GROUPS).flat(),
|
||||||
|
];
|
||||||
|
console.log("History compaction before", before);
|
||||||
|
console.log("History compaction after", {
|
||||||
|
message_count: messages.length,
|
||||||
|
turn_group_count: messages.filter((message) => message.role === "user").length,
|
||||||
|
estimated_tokens: Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(JSON.stringify([{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages]).length / ESTIMATED_CHARS_PER_TOKEN)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return minuteBucket;
|
||||||
|
} finally {
|
||||||
|
setStatus("Thinking…");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runAgentTurn(userText) {
|
async function runAgentTurn(userText) {
|
||||||
const apiKey = apiKeyEl.value.trim();
|
const apiKey = apiKeyEl.value.trim();
|
||||||
if (!apiKey) throw new Error("Missing API key");
|
if (!apiKey) throw new Error("Missing API key");
|
||||||
@@ -525,18 +630,29 @@ async function runAgentTurn(userText) {
|
|||||||
const provider = PROVIDERS[getProviderValue()];
|
const provider = PROVIDERS[getProviderValue()];
|
||||||
const { chat } = provider.api;
|
const { chat } = provider.api;
|
||||||
const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined;
|
const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined;
|
||||||
|
let firstIterationMinuteBucket = null;
|
||||||
|
|
||||||
messages.push({ role: "user", content: userText });
|
messages.push({ role: "user", content: userText });
|
||||||
trimHistory();
|
|
||||||
|
|
||||||
for (let i = 0; i < 64; i++) {
|
for (let i = 0; i < 64; i++) {
|
||||||
const response = await chat({
|
const compactedMinuteBucket = await compactHistoryWithLLM(chat, apiKey, baseURL, modelEl.value);
|
||||||
|
if (firstIterationMinuteBucket === null && compactedMinuteBucket !== null) {
|
||||||
|
firstIterationMinuteBucket = compactedMinuteBucket;
|
||||||
|
}
|
||||||
|
if (messages.length > MAX_HISTORY_MESSAGES * 2) trimHistory();
|
||||||
|
|
||||||
|
const messages_with_system = [{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages];
|
||||||
|
const { minuteBucket, response } = await chatWithMinuteDelay({
|
||||||
|
chat,
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL,
|
baseURL,
|
||||||
model: modelEl.value,
|
model: modelEl.value,
|
||||||
messages: [{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages],
|
messages: messages_with_system,
|
||||||
tools,
|
tools,
|
||||||
});
|
});
|
||||||
|
if (firstIterationMinuteBucket === null) {
|
||||||
|
firstIterationMinuteBucket = minuteBucket;
|
||||||
|
}
|
||||||
|
|
||||||
const message = response.choices?.[0]?.message;
|
const message = response.choices?.[0]?.message;
|
||||||
if (!message) throw new Error("No message in response");
|
if (!message) throw new Error("No message in response");
|
||||||
@@ -546,7 +662,10 @@ async function runAgentTurn(userText) {
|
|||||||
if (message.content) addMessage("assistant", message.content);
|
if (message.content) addMessage("assistant", message.content);
|
||||||
|
|
||||||
const calls = message.tool_calls ?? [];
|
const calls = message.tool_calls ?? [];
|
||||||
if (calls.length === 0) return;
|
if (calls.length === 0) {
|
||||||
|
console.log("Estimated token usage by minute", getEstimatedTokenMinuteLog(firstIterationMinuteBucket));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const call of calls) {
|
for (const call of calls) {
|
||||||
let args = {};
|
let args = {};
|
||||||
|
|||||||
@@ -111,6 +111,10 @@
|
|||||||
<span class="spinner"></span>
|
<span class="spinner"></span>
|
||||||
<span>thinking...</span>
|
<span>thinking...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="thinking-indicator" id="compactingIndicator" hidden>
|
||||||
|
<span class="spinner"></span>
|
||||||
|
<span>compacting...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="composer">
|
<div class="composer">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@@ -618,7 +618,7 @@ void eliminate_overlaps(DebugWriter& debug_writer, double OVERLAP_RESOLUTION_DIS
|
|||||||
std::cerr << "processing: " << edge.first << " " << edge.second << std::endl;
|
std::cerr << "processing: " << edge.first << " " << edge.second << std::endl;
|
||||||
std::cerr << "area before: " << poly1->area() << " " << poly2->area() << std::endl;
|
std::cerr << "area before: " << poly1->area() << " " << poly2->area() << std::endl;
|
||||||
|
|
||||||
bool is_ = edge == std::make_pair(25, 27);
|
bool is_ = edge == std::make_pair<size_t, size_t>(25, 27);
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) {
|
if ((mp1 = maybe_take_first_if_single_item(create_and_convert_offset_polygon(OVERLAP_RESOLUTION_DISTANCE, *poly2)))) {
|
||||||
@@ -1550,7 +1550,7 @@ std::vector<K::FT> arrangement_cell_iou(Arrangement_2& left, Arrangement_2& righ
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto res = walk_pl.locate(best_point);
|
auto res = walk_pl.locate(best_point);
|
||||||
if (auto* v = boost::get<Arrangement_2::Face_const_handle>(&res)) {
|
if (auto* v = variant_get<Arrangement_2::Face_const_handle>(&res)) {
|
||||||
if (visited_faces_on_right.count(*v) > 0) {
|
if (visited_faces_on_right.count(*v) > 0) {
|
||||||
return_values.push_back(0);
|
return_values.push_back(0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user