mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-12 09:13:17 +08:00
实现附件处理链路的统一引用化与可检索增强,避免大文件/多模态内容直接撑爆上下文并提升工具可用性。
本次补齐 text/image/video/archive 的标准化处理、会话级工具守卫、回放压缩、配置与文档对齐,并修复表格与流式输出相关体验问题。 Made-with: Cursor
This commit is contained in:
parent
9d2900db02
commit
37a2ef35f4
27 changed files with 3227 additions and 278 deletions
|
|
@ -6,10 +6,10 @@ import io
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
|
@ -18,8 +18,24 @@ from PIL import Image
|
|||
from PyPDF2 import PdfReader
|
||||
|
||||
from oclaw.platform.config.paths import PROJECT_ROOT
|
||||
from oclaw.platform.files.archive_processor import (
|
||||
MAX_ARCHIVE_DEPTH,
|
||||
MAX_ARCHIVE_ENTRY_BYTES,
|
||||
MAX_ARCHIVE_FILE_COUNT,
|
||||
MAX_ARCHIVE_MEMBER_NAME_LENGTH,
|
||||
MAX_ARCHIVE_TOTAL_UNCOMPRESSED_BYTES,
|
||||
detect_archive_kind,
|
||||
is_safe_archive_member_name,
|
||||
process_archive,
|
||||
)
|
||||
from oclaw.platform.files.attachment_assets import AttachmentAssetStore
|
||||
from oclaw.platform.files.tabular_attachment_store import save_dataframe, save_workbook
|
||||
from oclaw.platform.files.text_attachment_store import (
|
||||
DEFAULT_TEXT_CHUNK_OVERLAP,
|
||||
DEFAULT_TEXT_CHUNK_SIZE,
|
||||
DEFAULT_TEXT_INLINE_MAX_CHARS,
|
||||
save_text_document,
|
||||
)
|
||||
|
||||
|
||||
def _sniff_image_mime(data: bytes) -> str | None:
|
||||
|
|
@ -36,25 +52,101 @@ def _sniff_image_mime(data: bytes) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _ffprobe_exists() -> bool:
|
||||
try:
|
||||
p = subprocess.run(["ffprobe", "-version"], capture_output=True, text=True, timeout=3)
|
||||
return p.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _extract_video_meta_from_ffprobe(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
fmt = obj.get("format") if isinstance(obj.get("format"), dict) else {}
|
||||
streams = obj.get("streams") if isinstance(obj.get("streams"), list) else []
|
||||
duration_sec = None
|
||||
if isinstance(fmt, dict) and fmt.get("duration") is not None:
|
||||
try:
|
||||
duration_sec = float(fmt.get("duration"))
|
||||
except Exception:
|
||||
duration_sec = None
|
||||
width = None
|
||||
height = None
|
||||
fps = None
|
||||
for s in streams:
|
||||
if not isinstance(s, dict):
|
||||
continue
|
||||
if str(s.get("codec_type") or "") != "video":
|
||||
continue
|
||||
if s.get("width") is not None and s.get("height") is not None:
|
||||
try:
|
||||
width = int(s.get("width") or 0) or None
|
||||
height = int(s.get("height") or 0) or None
|
||||
except Exception:
|
||||
width = width
|
||||
height = height
|
||||
fr = str(s.get("avg_frame_rate") or s.get("r_frame_rate") or "").strip()
|
||||
if fr and fr != "0/0" and "/" in fr:
|
||||
try:
|
||||
a, b = fr.split("/", 1)
|
||||
fa = float(a)
|
||||
fb = float(b)
|
||||
if fb:
|
||||
fps = fa / fb
|
||||
except Exception:
|
||||
fps = fps
|
||||
break
|
||||
out: dict[str, Any] = {}
|
||||
if duration_sec is not None:
|
||||
out["duration_sec"] = duration_sec
|
||||
if width is not None:
|
||||
out["width"] = width
|
||||
if height is not None:
|
||||
out["height"] = height
|
||||
if fps is not None:
|
||||
out["fps"] = fps
|
||||
return out
|
||||
|
||||
|
||||
def _ffprobe_video_meta(path: Path) -> dict[str, Any]:
|
||||
if not _ffprobe_exists():
|
||||
return {}
|
||||
try:
|
||||
p = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
return {}
|
||||
obj = json.loads(p.stdout or "{}")
|
||||
if not isinstance(obj, dict):
|
||||
return {}
|
||||
return _extract_video_meta_from_ffprobe(obj)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
MAX_TEXT_CHARS = 12_000
|
||||
EXCEL_PREVIEW_ROWS = 80
|
||||
LARGE_TABLE_PREVIEW_ROWS = 20
|
||||
MAX_ZIP_DEPTH = 2
|
||||
MAX_ZIP_FILE_COUNT = 200
|
||||
MAX_ZIP_ENTRY_BYTES = 10 * 1024 * 1024
|
||||
MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES = 50 * 1024 * 1024
|
||||
MAX_ZIP_DEPTH = MAX_ARCHIVE_DEPTH
|
||||
MAX_ZIP_FILE_COUNT = MAX_ARCHIVE_FILE_COUNT
|
||||
MAX_ZIP_ENTRY_BYTES = MAX_ARCHIVE_ENTRY_BYTES
|
||||
MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES = MAX_ARCHIVE_TOTAL_UNCOMPRESSED_BYTES
|
||||
DEFAULT_TABULAR_ROWS_READ = 5000
|
||||
DEFAULT_TABULAR_COLUMNS = 200
|
||||
DEFAULT_TABULAR_CELL_CHARS = 500
|
||||
DEFAULT_TABULAR_TOOL_MODE_ENABLED = True
|
||||
DEFAULT_TABULAR_TOOL_MODE_MIN_ROWS = 20_000
|
||||
DEFAULT_TABULAR_TOOL_MODE_MIN_ROWS = 5_000
|
||||
DEFAULT_TABULAR_TOOL_MODE_MAX_BYTES = 30 * 1024 * 1024
|
||||
DEFAULT_LARGE_TABLE_PREVIEW_ROWS = LARGE_TABLE_PREVIEW_ROWS
|
||||
MAX_ZIP_MEMBER_NAME_LENGTH = 240
|
||||
MAX_ZIP_MEMBER_NAME_LENGTH = MAX_ARCHIVE_MEMBER_NAME_LENGTH
|
||||
MAX_EXCEL_ZIP_FILE_COUNT = 500
|
||||
MAX_EXCEL_ZIP_ENTRY_BYTES = 20 * 1024 * 1024
|
||||
MAX_EXCEL_ZIP_TOTAL_UNCOMPRESSED_BYTES = 120 * 1024 * 1024
|
||||
DEFAULT_MAX_EXCEL_SHEETS = 50
|
||||
DEFAULT_TEXT_QUERY_TOP_K = 5
|
||||
|
||||
|
||||
def _truncate_text(text: str, limit: int = MAX_TEXT_CHARS) -> str:
|
||||
|
|
@ -64,6 +156,24 @@ def _truncate_text(text: str, limit: int = MAX_TEXT_CHARS) -> str:
|
|||
return t[:limit] + f"\n\n...[truncated to {limit} chars]"
|
||||
|
||||
|
||||
def _text_ref_summary(*, name: str, chars: int, chunks: int, source_kind: str, preview: str, top_k: int) -> str:
|
||||
return _truncate_text(
|
||||
(
|
||||
f"# Text Document Summary\n"
|
||||
f"- name: {name}\n"
|
||||
f"- source_kind: {source_kind}\n"
|
||||
f"- chars: {chars}\n"
|
||||
f"- chunks: {chunks}\n"
|
||||
f"- mode: text_ref\n"
|
||||
f"- query_tool: query_text_attachment\n"
|
||||
f"- note: use `text_id` from accompanying `text_ref` attachment; recommended top_k <= {top_k}\n\n"
|
||||
f"## Preview (first {min(len(preview), 1500)} chars)\n"
|
||||
f"{preview[:1500]}"
|
||||
),
|
||||
limit=MAX_TEXT_CHARS,
|
||||
)
|
||||
|
||||
|
||||
def _decode_text_bytes(data: bytes) -> str:
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
|
|
@ -120,6 +230,26 @@ def _attachments_limits() -> dict[str, Any]:
|
|||
"tool_mode_max_bytes": _safe_int(
|
||||
tabular_cfg.get("tool_mode_max_bytes"), DEFAULT_TABULAR_TOOL_MODE_MAX_BYTES, max_value=500 * 1024 * 1024
|
||||
),
|
||||
"text_inline_max_chars": _safe_int(
|
||||
tabular_cfg.get("text_inline_max_chars"), DEFAULT_TEXT_INLINE_MAX_CHARS, max_value=200_000
|
||||
),
|
||||
"text_chunk_size": _safe_int(tabular_cfg.get("text_chunk_size"), DEFAULT_TEXT_CHUNK_SIZE, max_value=8_000),
|
||||
"text_chunk_overlap": _safe_int(
|
||||
tabular_cfg.get("text_chunk_overlap"), DEFAULT_TEXT_CHUNK_OVERLAP, max_value=4_000
|
||||
),
|
||||
"text_query_top_k": _safe_int(tabular_cfg.get("text_query_top_k"), DEFAULT_TEXT_QUERY_TOP_K, max_value=50),
|
||||
"archive_max_depth": _safe_int(tabular_cfg.get("archive_max_depth"), MAX_ARCHIVE_DEPTH, max_value=10),
|
||||
"archive_max_file_count": _safe_int(
|
||||
tabular_cfg.get("archive_max_file_count"), MAX_ARCHIVE_FILE_COUNT, max_value=20_000
|
||||
),
|
||||
"archive_max_entry_bytes": _safe_int(
|
||||
tabular_cfg.get("archive_max_entry_bytes"), MAX_ARCHIVE_ENTRY_BYTES, max_value=2_000_000_000
|
||||
),
|
||||
"archive_max_total_uncompressed_bytes": _safe_int(
|
||||
tabular_cfg.get("archive_max_total_uncompressed_bytes"),
|
||||
MAX_ARCHIVE_TOTAL_UNCOMPRESSED_BYTES,
|
||||
max_value=5_000_000_000,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -160,6 +290,11 @@ def _dataframe_summary_text(
|
|||
dtypes = [f"{str(k)}:{str(v)}" for k, v in df.dtypes.items()]
|
||||
show_rows = max(1, int(preview_rows or EXCEL_PREVIEW_ROWS))
|
||||
sample = df.head(show_rows).to_string(index=False)
|
||||
rows_scope_line = (
|
||||
f"## Preview (first {min(rows, show_rows)} rows)"
|
||||
if rows > show_rows
|
||||
else f"## Full table included ({rows} rows)"
|
||||
)
|
||||
body = (
|
||||
f"# Table Summary\n"
|
||||
f"- rows: {rows}\n"
|
||||
|
|
@ -169,7 +304,7 @@ def _dataframe_summary_text(
|
|||
f"- clipped_cells: {'yes' if clipped_cells else 'no'}\n"
|
||||
f"- columns: {', '.join(header)}\n"
|
||||
f"- dtypes: {', '.join(dtypes)}\n\n"
|
||||
f"## Preview (first {min(rows, show_rows)} rows)\n"
|
||||
f"{rows_scope_line}\n"
|
||||
f"{sample}"
|
||||
)
|
||||
return _truncate_text(body)
|
||||
|
|
@ -191,19 +326,7 @@ def _pdf_summary_text(reader: PdfReader) -> str:
|
|||
|
||||
|
||||
def _is_safe_zip_member_name(name: str) -> bool:
|
||||
n = str(name or "").replace("\\", "/").strip()
|
||||
if not n:
|
||||
return False
|
||||
if len(n) > MAX_ZIP_MEMBER_NAME_LENGTH:
|
||||
return False
|
||||
if any(ord(ch) < 32 for ch in n):
|
||||
return False
|
||||
p = PurePosixPath(n)
|
||||
if p.is_absolute():
|
||||
return False
|
||||
if any(part in ("..", "") for part in p.parts):
|
||||
return False
|
||||
return True
|
||||
return is_safe_archive_member_name(name)
|
||||
|
||||
|
||||
def _validate_excel_zip_payload(data: bytes) -> str | None:
|
||||
|
|
@ -231,48 +354,21 @@ def _validate_excel_zip_payload(data: bytes) -> str | None:
|
|||
|
||||
|
||||
def process_zip(zip_data: bytes, *, _depth: int = 0) -> list[dict[str, Any]]:
|
||||
if _depth > MAX_ZIP_DEPTH:
|
||||
return [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "zip-error",
|
||||
"content": f"ZIP nesting too deep (>{MAX_ZIP_DEPTH})",
|
||||
}
|
||||
]
|
||||
attachments: list[dict[str, Any]] = []
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
infos = [x for x in zf.infolist() if not x.is_dir()]
|
||||
if len(infos) > MAX_ZIP_FILE_COUNT:
|
||||
raise ValueError(f"ZIP has too many files ({len(infos)} > {MAX_ZIP_FILE_COUNT})")
|
||||
total_uncompressed = 0
|
||||
for info in infos:
|
||||
if not _is_safe_zip_member_name(info.filename):
|
||||
raise ValueError(f"ZIP contains unsafe path: {info.filename}")
|
||||
total_uncompressed += int(info.file_size or 0)
|
||||
if int(info.file_size or 0) > MAX_ZIP_ENTRY_BYTES:
|
||||
raise ValueError(
|
||||
f"ZIP entry too large: {info.filename} ({int(info.file_size or 0)} > {MAX_ZIP_ENTRY_BYTES})"
|
||||
)
|
||||
if total_uncompressed > MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES:
|
||||
raise ValueError(
|
||||
"ZIP total uncompressed size too large "
|
||||
f"({total_uncompressed} > {MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES})"
|
||||
)
|
||||
with zf.open(info) as f:
|
||||
data = f.read()
|
||||
att = process_file_data(info.filename, data, _zip_depth=_depth + 1)
|
||||
if att:
|
||||
attachments.extend(att)
|
||||
except Exception as e:
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text",
|
||||
"name": "zip-error",
|
||||
"content": f"Error parsing zip: {e}",
|
||||
}
|
||||
)
|
||||
return attachments
|
||||
limits = _attachments_limits()
|
||||
return process_archive(
|
||||
archive_name="bundle.zip",
|
||||
archive_data=zip_data,
|
||||
process_member=lambda n, b, d: process_file_data(n, b, _zip_depth=d),
|
||||
depth=_depth,
|
||||
limits={
|
||||
"max_depth": int(limits.get("archive_max_depth") or MAX_ARCHIVE_DEPTH),
|
||||
"max_file_count": int(limits.get("archive_max_file_count") or MAX_ARCHIVE_FILE_COUNT),
|
||||
"max_entry_bytes": int(limits.get("archive_max_entry_bytes") or MAX_ARCHIVE_ENTRY_BYTES),
|
||||
"max_total_uncompressed_bytes": int(
|
||||
limits.get("archive_max_total_uncompressed_bytes") or MAX_ARCHIVE_TOTAL_UNCOMPRESSED_BYTES
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def process_file_data(name: str, data: bytes, *, _zip_depth: int = 0) -> list[dict[str, Any]]:
|
||||
|
|
@ -336,6 +432,67 @@ def process_file_data(name: str, data: bytes, *, _zip_depth: int = 0) -> list[di
|
|||
)
|
||||
return attachments
|
||||
|
||||
if ext in ("mp4", "mov", "mkv", "webm", "avi", "m4v"):
|
||||
mime = "video/mp4"
|
||||
if ext == "webm":
|
||||
mime = "video/webm"
|
||||
elif ext == "mov":
|
||||
mime = "video/quicktime"
|
||||
elif ext == "mkv":
|
||||
mime = "video/x-matroska"
|
||||
elif ext == "avi":
|
||||
mime = "video/x-msvideo"
|
||||
meta = AttachmentAssetStore().save_bytes(data, filename=name, mime=mime)
|
||||
probe_meta: dict[str, Any] = {}
|
||||
try:
|
||||
p = AttachmentAssetStore().get_local_path(str(meta.attachment_id))
|
||||
if p is not None:
|
||||
probe_meta = _ffprobe_video_meta(p)
|
||||
except Exception:
|
||||
probe_meta = {}
|
||||
attachments.append(
|
||||
{
|
||||
"type": "video_ref",
|
||||
"name": meta.name,
|
||||
"mime": meta.mime,
|
||||
"attachment_id": meta.attachment_id,
|
||||
"bytes": meta.bytes,
|
||||
**probe_meta,
|
||||
}
|
||||
)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text",
|
||||
"name": name,
|
||||
"content": _truncate_text(
|
||||
"".join(
|
||||
[
|
||||
"# Video Attachment\n",
|
||||
f"- name: {meta.name}\n",
|
||||
f"- mime: {meta.mime}\n",
|
||||
f"- bytes: {meta.bytes}\n",
|
||||
(
|
||||
f"- duration_sec: {probe_meta.get('duration_sec')}\n"
|
||||
if probe_meta.get("duration_sec") is not None
|
||||
else ""
|
||||
),
|
||||
(
|
||||
f"- size: {probe_meta.get('width')}x{probe_meta.get('height')}\n"
|
||||
if probe_meta.get("width") and probe_meta.get("height")
|
||||
else ""
|
||||
),
|
||||
(f"- fps: {probe_meta.get('fps')}\n" if probe_meta.get("fps") is not None else ""),
|
||||
"- mode: video_ref\n",
|
||||
"- query_tool: query_video_attachment\n",
|
||||
"- note: use `attachment_id` from accompanying `video_ref` attachment.\n",
|
||||
]
|
||||
),
|
||||
limit=MAX_TEXT_CHARS,
|
||||
),
|
||||
}
|
||||
)
|
||||
return attachments
|
||||
|
||||
if ext in ("xlsx", "xls", "csv"):
|
||||
try:
|
||||
if ext == "xlsx":
|
||||
|
|
@ -453,14 +610,97 @@ def process_file_data(name: str, data: bytes, *, _zip_depth: int = 0) -> list[di
|
|||
try:
|
||||
doc = Document(io.BytesIO(data))
|
||||
text = "\n".join([p.text for p in doc.paragraphs])
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(text)})
|
||||
limits = _attachments_limits()
|
||||
inline_cap = int(limits.get("text_inline_max_chars") or DEFAULT_TEXT_INLINE_MAX_CHARS)
|
||||
if len(str(text or "")) <= inline_cap:
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(text, limit=inline_cap)})
|
||||
else:
|
||||
meta = AttachmentAssetStore().save_bytes(
|
||||
data,
|
||||
filename=name,
|
||||
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
)
|
||||
text_meta = save_text_document(
|
||||
attachment_id=str(meta.attachment_id),
|
||||
name=name,
|
||||
text=str(text or ""),
|
||||
source_kind="docx",
|
||||
chunk_size=int(limits.get("text_chunk_size") or DEFAULT_TEXT_CHUNK_SIZE),
|
||||
chunk_overlap=int(limits.get("text_chunk_overlap") or DEFAULT_TEXT_CHUNK_OVERLAP),
|
||||
)
|
||||
top_k = int(limits.get("text_query_top_k") or DEFAULT_TEXT_QUERY_TOP_K)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text",
|
||||
"name": name,
|
||||
"content": _text_ref_summary(
|
||||
name=name,
|
||||
chars=int(text_meta.get("chars") or 0),
|
||||
chunks=int(text_meta.get("chunks") or 0),
|
||||
source_kind="docx",
|
||||
preview=str(text or ""),
|
||||
top_k=top_k,
|
||||
),
|
||||
}
|
||||
)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text_ref",
|
||||
"name": str(name),
|
||||
"attachment_id": str(meta.attachment_id),
|
||||
"text_id": str(text_meta.get("text_id") or ""),
|
||||
"chars": int(text_meta.get("chars") or 0),
|
||||
"chunks": int(text_meta.get("chunks") or 0),
|
||||
"source_kind": "docx",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(f"Error parsing docx: {e}")})
|
||||
|
||||
elif ext == "pdf":
|
||||
try:
|
||||
reader = PdfReader(io.BytesIO(data))
|
||||
attachments.append({"type": "text", "name": name, "content": _pdf_summary_text(reader)})
|
||||
text = _pdf_summary_text(reader)
|
||||
limits = _attachments_limits()
|
||||
inline_cap = int(limits.get("text_inline_max_chars") or DEFAULT_TEXT_INLINE_MAX_CHARS)
|
||||
if len(str(text or "")) <= inline_cap:
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(text, limit=inline_cap)})
|
||||
else:
|
||||
meta = AttachmentAssetStore().save_bytes(data, filename=name, mime="application/pdf")
|
||||
text_meta = save_text_document(
|
||||
attachment_id=str(meta.attachment_id),
|
||||
name=name,
|
||||
text=str(text or ""),
|
||||
source_kind="pdf",
|
||||
chunk_size=int(limits.get("text_chunk_size") or DEFAULT_TEXT_CHUNK_SIZE),
|
||||
chunk_overlap=int(limits.get("text_chunk_overlap") or DEFAULT_TEXT_CHUNK_OVERLAP),
|
||||
)
|
||||
top_k = int(limits.get("text_query_top_k") or DEFAULT_TEXT_QUERY_TOP_K)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text",
|
||||
"name": name,
|
||||
"content": _text_ref_summary(
|
||||
name=name,
|
||||
chars=int(text_meta.get("chars") or 0),
|
||||
chunks=int(text_meta.get("chunks") or 0),
|
||||
source_kind="pdf",
|
||||
preview=str(text or ""),
|
||||
top_k=top_k,
|
||||
),
|
||||
}
|
||||
)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text_ref",
|
||||
"name": str(name),
|
||||
"attachment_id": str(meta.attachment_id),
|
||||
"text_id": str(text_meta.get("text_id") or ""),
|
||||
"chars": int(text_meta.get("chars") or 0),
|
||||
"chunks": int(text_meta.get("chunks") or 0),
|
||||
"source_kind": "pdf",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(f"Error parsing pdf: {e}")})
|
||||
|
||||
|
|
@ -468,10 +708,65 @@ def process_file_data(name: str, data: bytes, *, _zip_depth: int = 0) -> list[di
|
|||
text = _decode_text_bytes(data)
|
||||
if ext == "html":
|
||||
text = _clean_html_to_text(text)
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(text)})
|
||||
limits = _attachments_limits()
|
||||
inline_cap = int(limits.get("text_inline_max_chars") or DEFAULT_TEXT_INLINE_MAX_CHARS)
|
||||
if len(str(text or "")) <= inline_cap:
|
||||
attachments.append({"type": "text", "name": name, "content": _truncate_text(text, limit=inline_cap)})
|
||||
else:
|
||||
meta = AttachmentAssetStore().save_bytes(data, filename=name, mime="text/plain")
|
||||
text_meta = save_text_document(
|
||||
attachment_id=str(meta.attachment_id),
|
||||
name=name,
|
||||
text=str(text or ""),
|
||||
source_kind=ext or "text",
|
||||
chunk_size=int(limits.get("text_chunk_size") or DEFAULT_TEXT_CHUNK_SIZE),
|
||||
chunk_overlap=int(limits.get("text_chunk_overlap") or DEFAULT_TEXT_CHUNK_OVERLAP),
|
||||
)
|
||||
top_k = int(limits.get("text_query_top_k") or DEFAULT_TEXT_QUERY_TOP_K)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text",
|
||||
"name": name,
|
||||
"content": _text_ref_summary(
|
||||
name=name,
|
||||
chars=int(text_meta.get("chars") or 0),
|
||||
chunks=int(text_meta.get("chunks") or 0),
|
||||
source_kind=str(text_meta.get("source_kind") or ext or "text"),
|
||||
preview=str(text or ""),
|
||||
top_k=top_k,
|
||||
),
|
||||
}
|
||||
)
|
||||
attachments.append(
|
||||
{
|
||||
"type": "text_ref",
|
||||
"name": str(name),
|
||||
"attachment_id": str(meta.attachment_id),
|
||||
"text_id": str(text_meta.get("text_id") or ""),
|
||||
"chars": int(text_meta.get("chars") or 0),
|
||||
"chunks": int(text_meta.get("chunks") or 0),
|
||||
"source_kind": str(text_meta.get("source_kind") or ext or "text"),
|
||||
}
|
||||
)
|
||||
|
||||
elif ext == "zip":
|
||||
attachments.extend(process_zip(data, _depth=_zip_depth))
|
||||
elif detect_archive_kind(name):
|
||||
limits = _attachments_limits()
|
||||
attachments.extend(
|
||||
process_archive(
|
||||
archive_name=name,
|
||||
archive_data=data,
|
||||
process_member=lambda n, b, d: process_file_data(n, b, _zip_depth=d),
|
||||
depth=_zip_depth,
|
||||
limits={
|
||||
"max_depth": int(limits.get("archive_max_depth") or MAX_ARCHIVE_DEPTH),
|
||||
"max_file_count": int(limits.get("archive_max_file_count") or MAX_ARCHIVE_FILE_COUNT),
|
||||
"max_entry_bytes": int(limits.get("archive_max_entry_bytes") or MAX_ARCHIVE_ENTRY_BYTES),
|
||||
"max_total_uncompressed_bytes": int(
|
||||
limits.get("archive_max_total_uncompressed_bytes") or MAX_ARCHIVE_TOTAL_UNCOMPRESSED_BYTES
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return attachments
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue