1 Commits

Author SHA1 Message Date
dependabot[bot] f0e1d3f872 chore(deps): bump the python group across 1 directory with 4 updates
Updates the requirements on [httpcore](https://github.com/encode/httpcore), [pydantic-settings](https://github.com/pydantic/pydantic-settings), [mcp](https://github.com/modelcontextprotocol/python-sdk) and [markitdown](https://github.com/microsoft/markitdown) to permit the latest version.

Updates `httpcore` to 1.0.9
- [Release notes](https://github.com/encode/httpcore/releases)
- [Changelog](https://github.com/encode/httpcore/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpcore/compare/1.0.0...1.0.9)

Updates `pydantic-settings` to 2.14.2
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.14.1...v2.14.2)

Updates `mcp` to 2.0.0
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v0.2.0...v2.0.0)

Updates `markitdown` from 0.1.6 to 0.1.7
- [Release notes](https://github.com/microsoft/markitdown/releases)
- [Commits](https://github.com/microsoft/markitdown/compare/v0.1.6...v0.1.7)

---
updated-dependencies:
- dependency-name: httpcore
  dependency-version: 1.0.9
  dependency-type: direct:production
  dependency-group: python
- dependency-name: markitdown
  dependency-version: 0.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python
- dependency-name: mcp
  dependency-version: 2.0.0
  dependency-type: direct:production
  dependency-group: python
- dependency-name: pydantic-settings
  dependency-version: 2.14.2
  dependency-type: direct:production
  dependency-group: python
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 17:40:16 +00:00
10 changed files with 90 additions and 140 deletions
+1 -1
View File
@@ -33,4 +33,4 @@ PyMuPDF
# magika (onnxruntime), already a core dep via fastembed. We avoid the
# [all]/Azure/audio extras (cloud + heavy). Pinned to a release >30 days old per
# the dependency-age discussion in issue #485.
markitdown[docx,pptx,xlsx,xls]==0.1.6
markitdown[docx,pptx,xlsx,xls]==0.1.7
+3 -3
View File
@@ -3,9 +3,9 @@ uvicorn
python-multipart
python-dotenv
httpx
httpcore>=1.0,<2.0
httpcore>=1.0.9,<2.0
pydantic>=2.13.4
pydantic-settings>=2.14.1
pydantic-settings>=2.14.2
SQLAlchemy
pypdf
beautifulsoup4
@@ -41,7 +41,7 @@ bcrypt
# Built-in servers use the v1 low-level Server decorator API. MCP SDK v2 is a
# breaking rewrite, so keep fresh installs on the maintained v1 line until the
# servers are migrated together.
mcp<2
mcp<3
pyotp
qrcode[pil]
croniter
+1 -5
View File
@@ -187,12 +187,8 @@ _FUNCTION_MODEL_NAME_RE = re.compile(
_FUNCTION_MODEL_PARAMS_OPEN_RE = re.compile(r"<parameters>\s*", re.IGNORECASE)
_FUNCTION_MODEL_PARAMS_CLOSE_RE = re.compile(r"</parameters>", re.IGNORECASE)
_QWEN_ROLE_MARKER_RE = re.compile(r"</?\|(?:assistant|assistan|user|system|tool)\|>?|</\|end\|>?", re.IGNORECASE)
# At least one pipe is required around `end`. Both pipes used to be optional
# (`\|?end\|?`), which also matched a bare `end` on its own line and deleted it
# from ordinary prose and from Ruby/Lua/shell snippets that close blocks with
# one; see #5547. `|end`, `end|`, `|end|` and `/|end|` still strip as before.
_QWEN_BARE_MARKER_RE = re.compile(
r"(?:^|[\t\r\n ])(?:/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|"
r"(?:^|[\t\r\n ])(?:\|?end\|?|/?\|end\|)(?=[\t\r\n ]|$)|"
r"(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)",
re.IGNORECASE,
)
+79 -4
View File
@@ -3908,10 +3908,85 @@ function startOdysseusApp() {
const messageInput = el('message');
const modelPickerWrap = document.getElementById('model-picker-wrap');
// ArrowUp/ArrowDown prompt recall on #message lives in
// static/js/composerArrowUpRecall.js (wired from chat.js). Do not re-add a
// copy here: two capture-phase listeners on the same textarea meant the one
// without the draft guard won and ate unsent multi-line prompts (#5862).
function _readComposerPromptHistory() {
const chatBox = document.getElementById('chat-history');
if (!chatBox) return [];
return Array.from(chatBox.querySelectorAll('.msg-user'))
.reverse()
.map(msg => {
const body = msg.querySelector('.body');
return msg.dataset?.raw || (body ? body.textContent : '') || '';
})
.filter(Boolean);
}
if (messageInput && !messageInput._odysseusPromptRecallCapture) {
messageInput._odysseusPromptRecallCapture = true;
let recallHistory = [];
let recallIndex = -1;
let lastRecalled = '';
const norm = (v) => String(v || '').replace(/\r\n/g, '\n').trimEnd();
messageInput.addEventListener('input', () => {
if (norm(messageInput.value) === norm(lastRecalled)) return;
recallHistory = [];
recallIndex = -1;
lastRecalled = '';
try { delete messageInput.dataset.odysseusRecallIndex; } catch {}
}, true);
messageInput.addEventListener('keydown', (e) => {
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
if (window._ghostAutocomplete?.isActive?.()) return;
const fresh = _readComposerPromptHistory();
const history = fresh.length ? fresh : recallHistory;
if (!history.length) return;
const current = norm(messageInput.value);
let currentIndex = current ? history.findIndex(item => norm(item) === current) : -1;
if (current && currentIndex < 0 && current === norm(lastRecalled)) currentIndex = recallIndex;
if (current && currentIndex < 0) {
const markedIndex = Number(messageInput.dataset.odysseusRecallIndex);
if (Number.isInteger(markedIndex) && markedIndex >= 0 && markedIndex < history.length) {
currentIndex = markedIndex;
}
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (e.key === 'ArrowDown') {
if (currentIndex < 0) return;
const nextIndex = currentIndex - 1;
if (nextIndex < 0) {
recallHistory = history;
recallIndex = -1;
lastRecalled = '';
try { delete messageInput.dataset.odysseusRecallIndex; } catch {}
messageInput.value = '';
try { messageInput.selectionStart = messageInput.selectionEnd = 0; } catch {}
try { uiModule.autoResize(messageInput); } catch {}
return;
}
const recalled = history[nextIndex];
recallHistory = history;
recallIndex = nextIndex;
lastRecalled = recalled;
try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {}
messageInput.value = recalled;
try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {}
try { uiModule.autoResize(messageInput); } catch {}
return;
}
const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0;
const recalled = history[nextIndex];
if (!recalled) return;
recallHistory = history;
recallIndex = nextIndex;
lastRecalled = recalled;
try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {}
messageInput.value = recalled;
try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {}
try { uiModule.autoResize(messageInput); } catch {}
}, true);
}
const _sendIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
const _micIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
+1 -1
View File
@@ -1005,7 +1005,7 @@
var tips = mobile ? phone : desktop;
var el = document.getElementById('welcome-tip');
if (el) {
el.textContent = tips[Math.floor(Math.random() * tips.length)];
el.textContent = 'Pick a model if you want, or just type.';
}
fetch('/api/version').then(function(r){return r.json()}).then(function(d){
if (d.version) window._appVersion = d.version;
+1 -2
View File
@@ -4787,8 +4787,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr
if (msgIndex < 0) return;
const bodyEl = userMsgElement.querySelector('.body');
let currentText = (userMsgElement.dataset.raw || (bodyEl ? bodyEl.textContent : '') || '').trim();
currentText = currentText.replace(/\s*\[\d+ attachment\(s\)\]$/, '');
const currentText = bodyEl ? bodyEl.textContent.trim().replace(/\s*\[\d+ attachment\(s\)\]$/, '') : '';
// Replace body with an editable textarea
const editor = document.createElement('textarea');
+1 -4
View File
@@ -478,10 +478,7 @@ const DSML_STRAY_RE = /<\s*\/?\s*[|]+\s*DSML\s*[|]+[^>]*>/gi;
const DSML_INVOKE_RE = /<\s*[|]+\s*DSML\s*[|]+\s*invoke\b[^>]*>[\s\S]*?(?:<\s*\/\s*[|]+\s*DSML\s*[|]+\s*invoke\s*>|$)/gi;
const RAW_OPENAI_TOOL_JSON_RE = /(?:\[\s*)?\{\s*"function"\s*:\s*\{[\s\S]*?\}\s*,\s*"id"\s*:\s*"[^"]*"\s*,\s*"type"\s*:\s*"function"\s*\}\s*\]?/gi;
const QWEN_ROLE_MARKER_RE = /<\/?\|(?:assistant|assistan|user|system|tool)\|>?|<\/\|end\|>?/gi;
// Keep in sync with _QWEN_BARE_MARKER_RE in src/tool_parsing.py. At least one
// pipe is required around `end`: with both optional (`\|?end\|?`) this also ate
// a bare `end` on its own line, breaking Ruby/Lua/shell snippets (#5547).
const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)/gi;
const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\|?end\|?|\/?\|end\|)(?=[\t\r\n ]|$)|(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)/gi;
// Self-narration about tool results (model echoing stdout/exit_code)
const TOOL_NARRATION_RE = /(?:The (?:result|output) shows?:?\s*)?-?\s*(?:stdout|stderr|exit_code):\s*.+/gi;
+3 -3
View File
@@ -143,9 +143,9 @@ export function wireArrowUpRecall(composer, getUserMessages, options = {}) {
return;
}
// ArrowUp walks older prompts. An unmatched draft already returned above,
// so reaching here means the composer is empty or holds a recalled prompt
// the caret-navigation case is never hijacked.
// ArrowUp owns prompt history in the chat composer. If the current text
// is not already a recalled prompt, start from newest instead of letting
// the browser move the caret inside the textarea.
const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0;
const recalled = history[nextIndex];
if (!recalled) {
-21
View File
@@ -306,24 +306,3 @@ def test_integration_recalls_from_chat_history_dom():
)
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout.strip()) == {"value": "stored prompt", "prevented": True}
def test_prompt_recall_is_not_duplicated_in_app_js():
"""Only composerArrowUpRecall.js may own ArrowUp on #message (issue #5862).
static/app.js once carried a near-verbatim copy of this recall logic, wired
as a second capture-phase listener on the same textarea. That copy lacked
the draft guard here, and because it called stopImmediatePropagation it won
regardless of registration order — so a typed multi-line prompt was replaced
by the last sent one instead of the caret moving up a line.
"""
app_js = (_REPO / "static" / "app.js").read_text(encoding="utf-8")
for marker in (
"_odysseusPromptRecallCapture",
"_readComposerPromptHistory",
"odysseusRecallIndex",
):
assert marker not in app_js, (
f"static/app.js reintroduces prompt recall ({marker!r}); "
"it belongs to static/js/composerArrowUpRecall.js alone"
)
@@ -1,96 +0,0 @@
"""Regression: the Qwen bare-marker scrub must not eat a lone `end` (#5547).
`_QWEN_BARE_MARKER_RE` cleans Qwen turn markers that leak into content. Its
`end` branch was `\\|?end\\|?` — both pipes optional — so it also matched a bare
`end` surrounded by whitespace and replaced it with a space. Any message
containing Ruby, Lua or shell code that closes a block with a lone `end` had
those lines silently deleted, in the stored text and in the rendered message.
Requiring at least one pipe keeps every real marker (`|end`, `end|`, `|end|`,
`/|end|`) stripping as before. The same pattern is duplicated in
static/js/chatRenderer.js, so the JS copy is checked here too — the two must
not drift.
"""
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
import src.agent_tools # noqa: F401 (break agent_tools<->tool_parsing import cycle)
from src.tool_parsing import strip_tool_blocks
_REPO = Path(__file__).resolve().parent.parent
_CHAT_RENDERER = _REPO / "static" / "js" / "chatRenderer.js"
# Inputs that must survive untouched, and the substring that proves they did.
KEPT = [
("loop do\n puts \"yo\"\nend\n", "\nend"), # the reported Ruby case
("if x then\nend", "\nend"),
("function f()\nend\n", "\nend"),
("a end b", "a end b"),
("append end", "append end"),
("END", "END"),
("\nEnd\n", "End"),
]
# Real markers — at least one pipe, plus the role word — with the exact output
# they must still produce. Asserted as equality rather than "marker not in out"
# so narrowing the pattern can't pass by deleting more than it should.
STRIPPED = [
("a |end| b", "a b"),
("a /|end| b", "a b"),
("a |end b", "a b"),
("a end| b", "a b"),
("x assistant y", "x y"),
]
@pytest.mark.parametrize("text,kept", KEPT)
def test_bare_end_survives_stripping(text, kept):
assert kept in strip_tool_blocks(text)
@pytest.mark.parametrize("text,expected", STRIPPED)
def test_piped_end_markers_are_still_stripped(text, expected):
assert strip_tool_blocks(text) == expected
def test_bare_end_inside_a_fenced_block_survives():
"""The scrub runs over the whole message, fenced regions included."""
out = strip_tool_blocks("Here:\n```ruby\nloop do\n puts 1\nend\n```\nDone.")
assert "\nend\n" in out
def _js_bare_marker_regex_source():
src = _CHAT_RENDERER.read_text(encoding="utf-8")
m = re.search(r"^const QWEN_BARE_MARKER_RE = (/.*/[gimsuy]*);$", src, re.MULTILINE)
assert m, "QWEN_BARE_MARKER_RE literal not found in chatRenderer.js"
return m.group(1)
def test_js_copy_of_the_pattern_matches_the_python_one():
"""Guard the duplication: the JS branch must require a pipe too."""
if shutil.which("node") is None:
pytest.skip("node binary not on PATH")
cases = [text for text, _ in KEPT] + [text for text, _ in STRIPPED]
script = (
"const RE = %s;\n"
"const cases = JSON.parse(process.argv[1]);\n"
"console.log(JSON.stringify(cases.map(c => c.replace(RE, ' '))));"
% _js_bare_marker_regex_source()
)
result = subprocess.run(
["node", "--input-type=module", "-e", script, json.dumps(cases)],
cwd=_REPO, capture_output=True, timeout=15, text=True,
)
assert result.returncode == 0, f"node failed:\n{result.stderr}"
got = json.loads(result.stdout.splitlines()[-1])
for (text, kept), out in zip(KEPT, got):
assert kept in out, f"JS regex dropped {kept!r} from {text!r}"
for (text, expected), out in zip(STRIPPED, got[len(KEPT):]):
assert out == expected, f"JS regex: {text!r} -> {out!r}, expected {expected!r}"