fix multi-query agent workspace searches
This commit is contained in:
@@ -1766,6 +1766,30 @@ async def run_agent(
|
||||
return await _execute_agent(request, user_id=user_id)
|
||||
|
||||
|
||||
def _explicit_workspace_search_queries(task: str) -> list[str]:
|
||||
"""Extract a small numbered list of explicit workspace search targets."""
|
||||
marker = re.search(r"(?:للعثور\s+على|to\s+find)\s*[::]", task, re.IGNORECASE)
|
||||
if marker is None:
|
||||
return []
|
||||
remainder = re.split(
|
||||
r"\s+(?:بعد\s+كل\s+بحث|ثم\s+لخّص|ثم\s+لخص|after\s+each\s+search|then\s+summari[sz]e)\b",
|
||||
task[marker.end() :],
|
||||
maxsplit=1,
|
||||
flags=re.IGNORECASE,
|
||||
)[0]
|
||||
item_pattern = re.compile(
|
||||
r"(?<![\w.])(?:\(\s*[1-5]\s*\)|[1-5][.)])\s*(.*?)"
|
||||
r"(?=(?:\s*[,،;]\s*)?(?:\(\s*[1-5]\s*\)|[1-5][.)])\s*|$)",
|
||||
re.DOTALL,
|
||||
)
|
||||
queries = [
|
||||
item.strip(" \t\r\n.,،;:")
|
||||
for item in item_pattern.findall(remainder)
|
||||
if item.strip(" \t\r\n.,،;:")
|
||||
]
|
||||
return queries[:MAX_AGENT_TOOL_CALLS] if len(queries) > 1 else []
|
||||
|
||||
|
||||
async def _execute_agent(
|
||||
request: AgentRequest,
|
||||
report_progress: Any | None = None,
|
||||
@@ -1812,6 +1836,9 @@ async def _execute_agent(
|
||||
for phrase in (
|
||||
"ابحث في ملفات المشروع",
|
||||
"ابحث في ملفات مساحة العمل",
|
||||
"البحث في مساحة العمل",
|
||||
"البحث في ملفات مساحة العمل",
|
||||
"أداة البحث في مساحة العمل",
|
||||
"ابحث في الملفات",
|
||||
"ابحث عن الملف",
|
||||
"ابحث داخل المشروع",
|
||||
@@ -1857,17 +1884,39 @@ async def _execute_agent(
|
||||
prefetched_knowledge = _knowledge_context_for_model(prefetched_results)
|
||||
|
||||
prefetched_workspace: list[dict[str, str]] = []
|
||||
explicit_workspace_queries = _explicit_workspace_search_queries(request.task)
|
||||
workspace_search_executed = (
|
||||
explicit_workspace_search
|
||||
and selected_workspace is not None
|
||||
and (selected_skill is None or "search_workspace" in selected_skill.allowed_tools)
|
||||
)
|
||||
if workspace_search_executed:
|
||||
await report("ينفذ البحث الصريح في مساحة العمل المسموحة.")
|
||||
matches = workspace.retrieve(request.task, selected_workspace)
|
||||
prefetched_workspace = [
|
||||
{"path": path, "excerpt": excerpt} for path, excerpt in matches
|
||||
]
|
||||
queries = explicit_workspace_queries or [request.task]
|
||||
matches_by_query: list[list[tuple[str, str]]] = []
|
||||
for query in queries:
|
||||
await report(
|
||||
f"يبحث قراءةً فقط في مساحة العمل عن: {query[:100]}"
|
||||
if explicit_workspace_queries
|
||||
else "ينفذ البحث الصريح في مساحة العمل المسموحة."
|
||||
)
|
||||
matches_by_query.append(
|
||||
workspace.retrieve(query, selected_workspace, limit=3)
|
||||
)
|
||||
seen_matches: set[tuple[str, str]] = set()
|
||||
context_chars = 0
|
||||
for result_index in range(3):
|
||||
for matches in matches_by_query:
|
||||
if result_index >= len(matches):
|
||||
continue
|
||||
path, excerpt = matches[result_index]
|
||||
identity = (path, excerpt)
|
||||
if identity in seen_matches:
|
||||
continue
|
||||
if len(prefetched_workspace) >= 6 or context_chars + len(excerpt) > 9_000:
|
||||
continue
|
||||
seen_matches.add(identity)
|
||||
prefetched_workspace.append({"path": path, "excerpt": excerpt})
|
||||
context_chars += len(excerpt)
|
||||
if selected_skill is None or "calculator" in selected_skill.allowed_tools:
|
||||
resolved_calculation = requested_workspace_constant_calculation(
|
||||
request.task, prefetched_workspace
|
||||
@@ -1931,7 +1980,7 @@ async def _execute_agent(
|
||||
}
|
||||
)
|
||||
if selected_workspace is not None:
|
||||
if not workspace_search_executed and (
|
||||
if (not workspace_search_executed or explicit_workspace_queries) and (
|
||||
selected_skill is None or "search_workspace" in selected_skill.allowed_tools
|
||||
):
|
||||
tools.append(
|
||||
@@ -2013,7 +2062,7 @@ async def _execute_agent(
|
||||
"content": (
|
||||
"أنت وكيل محلي يستخدم حتى ثلاث خطوات أدوات مسموحة بالتتابع، أداة واحدة في كل خطوة. استخدم calculator للحسابات عندما تحتاجها. "
|
||||
+ (
|
||||
"بحث الخادم في مساحة العمل المسموحة مسبقًا عن طلب المستخدم؛ استخدم المقتطفات المعروضة ولا تكرر البحث. "
|
||||
"بحث الخادم في مساحة العمل المسموحة مسبقًا عن الطلب؛ استخدم المقتطفات ولا تكرر أهداف البحث المكتملة. "
|
||||
if workspace_search_executed
|
||||
else "استخدم search_workspace عند الحاجة للعثور على مقاطع من مساحة العمل المسموحة. "
|
||||
)
|
||||
@@ -2084,6 +2133,17 @@ async def _execute_agent(
|
||||
# Reduce stochastic generic answers when the user supplied concrete files
|
||||
# and expects the agent to ground its response in their contents.
|
||||
payload["temperature"] = 0.0
|
||||
if (
|
||||
("heartbeat" in task_lower or "نبضة" in task_lower)
|
||||
and ("timeout" in task_lower or "مهلة" in task_lower)
|
||||
):
|
||||
payload["messages"][0]["content"] += (
|
||||
" اشرح كل قيمة من مقتطف تنفيذ مطابق، ولا تستنتج مهلة إجمالية للوكيل من مهلة استدعاء واحد. "
|
||||
"ميّز بين: heartbeat وهي فترة دورية لإرسال إشارة أثناء خمول SSE وليست مهلة اتصال؛ "
|
||||
"و`Stream.timeout` وهي مهلة خمول بين بيانات HTTP؛ و`get_completion(timeout_seconds=...)` "
|
||||
"وهي مهلة لاستدعاء نموذج واحد، لا للحلقة كاملة. إذا لم تجد حدًا إجماليًا صريحًا فقل ذلك. "
|
||||
"لا تعتبر هذه التعليمات دليلًا؛ الدليل هو الأسطر المسترجعة من ملفات التنفيذ."
|
||||
)
|
||||
if (
|
||||
selected_file_context
|
||||
and selected_skill is not None
|
||||
|
||||
@@ -476,6 +476,52 @@ def list_knowledge_files(root: Path) -> list[Path]:
|
||||
return files
|
||||
|
||||
|
||||
def _matching_excerpt(text: str, terms: set[str], max_chars: int) -> str:
|
||||
lines = text.splitlines()
|
||||
if not lines:
|
||||
return text[:max_chars]
|
||||
|
||||
scored_lines: list[tuple[int, int]] = []
|
||||
for index, line in enumerate(lines):
|
||||
words = set(re.findall(r"[\w\u0600-\u06ff]+", line.casefold()))
|
||||
matched = terms & words
|
||||
if matched:
|
||||
score = sum(2 if len(term) >= 8 else 1 for term in matched)
|
||||
scored_lines.append((score, index))
|
||||
if not scored_lines:
|
||||
return text[:max_chars]
|
||||
|
||||
windows: list[tuple[int, int, int]] = []
|
||||
for score, index in scored_lines:
|
||||
start = max(0, index - 1)
|
||||
end = min(len(lines), index + 2)
|
||||
windows.append((score, start, end))
|
||||
windows.sort(key=lambda item: (-item[0], item[1]))
|
||||
|
||||
selected: set[int] = set()
|
||||
for _score, start, end in windows:
|
||||
if any(line_number in selected for line_number in range(start, end)):
|
||||
continue
|
||||
candidate = set(range(start, end))
|
||||
rendered = [f"{line_number + 1}: {lines[line_number]}" for line_number in sorted(selected | candidate)]
|
||||
if len("\n".join(rendered)) > max_chars:
|
||||
continue
|
||||
selected.update(candidate)
|
||||
if not selected:
|
||||
_score, index = scored_lines[0]
|
||||
line = lines[index]
|
||||
matching_terms = [term for term in terms if term in set(re.findall(r"[\w\u0600-\u06ff]+", line.casefold()))]
|
||||
positions = [line.casefold().find(term) for term in matching_terms if term]
|
||||
if len(line) > max_chars and positions:
|
||||
start = max(0, positions[0] - max_chars // 3)
|
||||
line = ("…" if start else "") + line[start : start + max_chars - 32] + ("…" if start + max_chars - 32 < len(lines[index]) else "")
|
||||
return f"{index + 1}: {line}"[:max_chars]
|
||||
return "\n…\n".join(
|
||||
f"{line_number + 1}: {lines[line_number]}"
|
||||
for line_number in sorted(selected)
|
||||
)[:max_chars]
|
||||
|
||||
|
||||
def retrieve(task: str, root: Path, limit: int = 3) -> list[tuple[str, str]]:
|
||||
terms = {
|
||||
term.casefold()
|
||||
@@ -484,8 +530,26 @@ def retrieve(task: str, root: Path, limit: int = 3) -> list[tuple[str, str]]:
|
||||
"the", "and", "for", "with", "this", "that", "من", "على", "في", "عن",
|
||||
"كيف", "شو", "ما", "ماذا", "هذا", "هذه", "التي", "الذي", "اشرح", "دور",
|
||||
"ملف", "ملفات", "اذكر", "المستخدمة", "المستخدم", "المشروع", "التفسير",
|
||||
"استخدم", "أداة", "البحث", "مساحة", "العمل", "مرات", "منفصلة", "للعثور",
|
||||
"بعد", "كل", "ثم", "افحص", "النتيجة", "لخص", "اشرح", "اذكر", "علاقة",
|
||||
}
|
||||
}
|
||||
term_expansions = {
|
||||
"مهلة": {"timeout", "timeouts"},
|
||||
"عميل": {"client"},
|
||||
"مسار": {"path", "uri", "route"},
|
||||
"بث": {"stream"},
|
||||
"تدفق": {"stream"},
|
||||
"واجهة": {"api", "endpoint"},
|
||||
}
|
||||
terms.update(
|
||||
alias
|
||||
for term in tuple(terms)
|
||||
for alias in term_expansions.get(term, set())
|
||||
)
|
||||
code_search = bool(
|
||||
re.search(r"[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+|run/stream|\b(?:api|http|flutter|timeout)\b", task, re.IGNORECASE)
|
||||
)
|
||||
mentioned_paths = {
|
||||
match.replace("\\", "/").casefold()
|
||||
for match in re.findall(
|
||||
@@ -508,11 +572,25 @@ def retrieve(task: str, root: Path, limit: int = 3) -> list[tuple[str, str]]:
|
||||
relative = path.relative_to(root).as_posix()
|
||||
if relative.casefold() in mentioned_paths:
|
||||
score += 100_000
|
||||
if code_search:
|
||||
if path.suffix.casefold() in {".py", ".dart", ".js", ".ts", ".tsx", ".jsx"}:
|
||||
score += 30
|
||||
if "flutter" in terms and path.suffix.casefold() == ".dart":
|
||||
score += 60
|
||||
if path.suffix.casefold() in {".md", ".txt"}:
|
||||
score = max(1, score - 30)
|
||||
if any(part.casefold() in {"test", "tests"} for part in path.parts) and not any(
|
||||
term in terms for term in {"test", "tests", "اختبار", "اختبارات"}
|
||||
):
|
||||
score = max(1, score - 30)
|
||||
if score:
|
||||
# If the user named a source file explicitly, include a larger bounded
|
||||
# excerpt so the agent can explain the code rather than just locate it.
|
||||
excerpt_limit = 12_000 if relative.casefold() in mentioned_paths else 1_800
|
||||
ranked.append((score, relative, text[:excerpt_limit]))
|
||||
# Explicit paths need enough context for explanation; search results
|
||||
# should show the matching lines instead of an unrelated file prefix.
|
||||
if relative.casefold() in mentioned_paths:
|
||||
excerpt = text[:12_000]
|
||||
else:
|
||||
excerpt = _matching_excerpt(text, terms, 1_800)
|
||||
ranked.append((score, relative, excerpt))
|
||||
ranked.sort(key=lambda item: (-item[0], item[1]))
|
||||
explicit_matches = [
|
||||
item for item in ranked if item[1].casefold() in mentioned_paths
|
||||
|
||||
Reference in New Issue
Block a user