aixtools 0.3.7__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aixtools might be problematic. Click here for more details.

Files changed (104) hide show
  1. {aixtools-0.3.7 → aixtools-0.3.9}/PKG-INFO +1 -2
  2. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/_version.py +3 -3
  3. aixtools-0.3.9/aixtools/agents/prompt.py +97 -0
  4. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logfilters/context_filter.py +7 -2
  5. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/config.py +0 -23
  6. {aixtools-0.3.7 → aixtools-0.3.9}/pyproject.toml +1 -2
  7. aixtools-0.3.7/aixtools/agents/prompt.py +0 -175
  8. {aixtools-0.3.7 → aixtools-0.3.9}/README.md +0 -0
  9. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/config.toml +0 -0
  10. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/bn.json +0 -0
  11. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/en-US.json +0 -0
  12. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/gu.json +0 -0
  13. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/he-IL.json +0 -0
  14. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/hi.json +0 -0
  15. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/ja.json +0 -0
  16. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/kn.json +0 -0
  17. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/ml.json +0 -0
  18. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/mr.json +0 -0
  19. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/nl.json +0 -0
  20. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/ta.json +0 -0
  21. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/te.json +0 -0
  22. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/.chainlit/translations/zh-CN.json +0 -0
  23. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/__init__.py +0 -0
  24. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/app.py +0 -0
  25. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/auth_middleware.py +0 -0
  26. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/google_sdk/__init__.py +0 -0
  27. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +0 -0
  28. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +0 -0
  29. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/google_sdk/remote_agent_connection.py +0 -0
  30. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/google_sdk/utils.py +0 -0
  31. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/a2a/utils.py +0 -0
  32. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/__init__.py +0 -0
  33. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/agent.py +0 -0
  34. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/agent_batch.py +0 -0
  35. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/nodes_to_md.py +0 -0
  36. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/nodes_to_message.py +0 -0
  37. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/nodes_to_str.py +0 -0
  38. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/agents/print_nodes.py +0 -0
  39. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/app.py +0 -0
  40. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/auth/__init__.py +0 -0
  41. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/auth/auth.py +0 -0
  42. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/chainlit.md +0 -0
  43. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/compliance/__init__.py +0 -0
  44. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/compliance/private_data.py +0 -0
  45. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/context.py +0 -0
  46. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/db/__init__.py +0 -0
  47. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/db/database.py +0 -0
  48. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/db/vector_db.py +0 -0
  49. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/evals/__init__.py +0 -0
  50. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/evals/__main__.py +0 -0
  51. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/evals/dataset.py +0 -0
  52. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/evals/discovery.py +0 -0
  53. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/evals/run_evals.py +0 -0
  54. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/google/client.py +0 -0
  55. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/__init__.py +0 -0
  56. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/app.py +0 -0
  57. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/display.py +0 -0
  58. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/export.py +0 -0
  59. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/filters.py +0 -0
  60. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/log_utils.py +0 -0
  61. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/log_view/node_summary.py +0 -0
  62. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logfilters/__init__.py +0 -0
  63. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/__init__.py +0 -0
  64. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/log_objects.py +0 -0
  65. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/logging_config.py +0 -0
  66. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/mcp_log_models.py +0 -0
  67. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/mcp_logger.py +0 -0
  68. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/model_patch_logging.py +0 -0
  69. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/logging/open_telemetry.py +0 -0
  70. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/__init__.py +0 -0
  71. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/client.py +0 -0
  72. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/example_client.py +0 -0
  73. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/example_server.py +0 -0
  74. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/exceptions.py +0 -0
  75. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/fast_mcp_log.py +0 -0
  76. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/faulty_mcp.py +0 -0
  77. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/middleware.py +0 -0
  78. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/mcp/server.py +0 -0
  79. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/model_patch/model_patch.py +0 -0
  80. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/server/__init__.py +0 -0
  81. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/server/app_mounter.py +0 -0
  82. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/server/path.py +0 -0
  83. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/server/utils.py +0 -0
  84. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/testing/__init__.py +0 -0
  85. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/testing/agent_mock.py +0 -0
  86. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/testing/aix_test_model.py +0 -0
  87. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/testing/mock_tool.py +0 -0
  88. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/testing/model_patch_cache.py +0 -0
  89. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/tools/doctor/__init__.py +0 -0
  90. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/tools/doctor/mcp_tool_doctor.py +0 -0
  91. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/tools/doctor/tool_doctor.py +0 -0
  92. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/tools/doctor/tool_recommendation.py +0 -0
  93. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/__init__.py +0 -0
  94. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/chainlit/cl_agent_show.py +0 -0
  95. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/chainlit/cl_utils.py +0 -0
  96. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/config_util.py +0 -0
  97. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/enum_with_description.py +0 -0
  98. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/files.py +0 -0
  99. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/persisted_dict.py +0 -0
  100. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/utils/utils.py +0 -0
  101. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/vault/__init__.py +0 -0
  102. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools/vault/vault.py +0 -0
  103. {aixtools-0.3.7 → aixtools-0.3.9}/aixtools.egg-info/SOURCES.txt +0 -0
  104. {aixtools-0.3.7 → aixtools-0.3.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
@@ -26,7 +26,6 @@ Requires-Dist: rich>=14.0.0
26
26
  Requires-Dist: ruff>=0.11.6
27
27
  Requires-Dist: streamlit>=1.44.1
28
28
  Requires-Dist: watchdog>=6.0.0
29
- Requires-Dist: markitdown[docx,pdf,pptx,xls,xlsx]>=0.1.3
30
29
  Provides-Extra: test
31
30
  Requires-Dist: pyyaml; extra == "test"
32
31
  Provides-Extra: feature
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.3.7'
32
- __version_tuple__ = version_tuple = (0, 3, 7)
31
+ __version__ = version = '0.3.9'
32
+ __version_tuple__ = version_tuple = (0, 3, 9)
33
33
 
34
- __commit_id__ = commit_id = 'g889028f57'
34
+ __commit_id__ = commit_id = 'gf2f460fba'
@@ -0,0 +1,97 @@
1
+ """Prompt building utilities for Pydantic AI agent, including file handling and context management."""
2
+
3
+ import mimetypes
4
+ from pathlib import Path
5
+
6
+ from pydantic_ai import BinaryContent
7
+
8
+ from aixtools.context import SessionIdTuple
9
+ from aixtools.server import container_to_host_path
10
+ from aixtools.utils.files import is_text_content
11
+
12
+ CLAUDE_MAX_FILE_SIZE_IN_CONTEXT = 4 * 1024 * 1024 # Claude limit 4.5 MB for PDF files
13
+ CLAUDE_IMAGE_MAX_FILE_SIZE_IN_CONTEXT = (
14
+ 5 * 1024 * 1024
15
+ ) # Claude limit 5 MB for images, to avoid large image files in context
16
+
17
+
18
+ def should_be_included_into_context(
19
+ file_content: BinaryContent | str | None,
20
+ file_size: int,
21
+ *,
22
+ max_img_size_bytes: int = CLAUDE_IMAGE_MAX_FILE_SIZE_IN_CONTEXT,
23
+ max_file_size_bytes: int = CLAUDE_MAX_FILE_SIZE_IN_CONTEXT,
24
+ ) -> bool:
25
+ """Decide whether a file content should be included into the model context based on its type and size."""
26
+ if not isinstance(file_content, BinaryContent):
27
+ return False
28
+
29
+ if file_content.media_type.startswith("text/"):
30
+ return False
31
+
32
+ # Exclude archive files as they're not supported by OpenAI models
33
+ archive_types = {
34
+ "application/zip",
35
+ "application/x-tar",
36
+ "application/gzip",
37
+ "application/x-gzip",
38
+ "application/x-rar-compressed",
39
+ "application/x-7z-compressed",
40
+ }
41
+ if file_content.media_type in archive_types:
42
+ return False
43
+
44
+ if file_content.is_image and file_size < max_img_size_bytes:
45
+ return True
46
+
47
+ return file_size < max_file_size_bytes
48
+
49
+
50
+ def file_to_binary_content(file_path: str | Path, mime_type: str = "") -> str | BinaryContent:
51
+ """
52
+ Read a file and return its content as either a UTF-8 string (for text files)
53
+ or BinaryContent (for binary files).
54
+ """
55
+ with open(file_path, "rb") as f:
56
+ data = f.read()
57
+
58
+ if not mime_type:
59
+ mime_type, _ = mimetypes.guess_type(file_path)
60
+ mime_type = mime_type or "application/octet-stream"
61
+
62
+ if is_text_content(data, mime_type):
63
+ return data.decode("utf-8")
64
+
65
+ return BinaryContent(data=data, media_type=mime_type)
66
+
67
+
68
+ def build_user_input(
69
+ session_tuple: SessionIdTuple,
70
+ user_text: str,
71
+ file_paths: list[Path],
72
+ ) -> str | list[str | BinaryContent]:
73
+ """Build user input for the Pydantic AI agent, including file attachments if provided."""
74
+ if not file_paths:
75
+ return user_text
76
+
77
+ attachment_info_lines = []
78
+ binary_attachments = []
79
+
80
+ for workspace_path in file_paths:
81
+ host_path = container_to_host_path(workspace_path, ctx=session_tuple)
82
+ file_size = host_path.stat().st_size
83
+ mime_type, _ = mimetypes.guess_type(host_path)
84
+ mime_type = mime_type or "application/octet-stream"
85
+
86
+ attachment_info = f"* {workspace_path.name} (file_size={file_size} bytes) (path in workspace: {workspace_path})"
87
+ binary_content = file_to_binary_content(host_path, mime_type)
88
+
89
+ if should_be_included_into_context(binary_content, file_size):
90
+ binary_attachments.append(binary_content)
91
+ attachment_info += f" -- provided to model context at index {len(binary_attachments) - 1}"
92
+
93
+ attachment_info_lines.append(attachment_info)
94
+
95
+ full_prompt = user_text + "\nAttachments:\n" + "\n".join(attachment_info_lines)
96
+
97
+ return [full_prompt] + binary_attachments
@@ -53,8 +53,13 @@ class ContextFilter(logging.Filter): # pylint: disable=too-few-public-methods
53
53
  except ImportError:
54
54
  pass
55
55
 
56
- if not user_id and not session_id:
57
- user_id, session_id = self._extract_from_mcp_context()
56
+ mcp_user_id = None
57
+ mcp_session_id = None
58
+ if not user_id or not session_id:
59
+ mcp_user_id, mcp_session_id = self._extract_from_mcp_context()
60
+
61
+ user_id = user_id or mcp_user_id
62
+ session_id = session_id or mcp_session_id
58
63
 
59
64
  context = ""
60
65
  if session_id and not str(session_id).startswith("default"):
@@ -146,26 +146,3 @@ APP_DEFAULT_SCOPE = get_variable_env("APP_DEFAULT_SCOPE", allow_empty=True)
146
146
  AUTH_TEST_TOKEN = get_variable_env("AUTH_TEST_TOKEN", allow_empty=True)
147
147
 
148
148
  MCP_TOOLS_MAX_RETRIES = int(get_variable_env("MCP_TOOLS_MAX_RETRIES", default=10))
149
-
150
-
151
- # File attachment limits and supported types for model context
152
- # Maximum extracted document text size (5MB default, planned for future use)
153
- MAX_EXTRACTED_TEXT_SIZE = int(get_variable_env("MAX_EXTRACTED_TEXT_SIZE", default=str(5 * 1024 * 1024)))
154
- # Maximum image attachment size (2MB default)
155
- MAX_IMAGE_ATTACHMENT_SIZE = int(get_variable_env("MAX_IMAGE_ATTACHMENT_SIZE", default=str(2 * 1024 * 1024)))
156
- # Image MIME types that can be attached to model context
157
- IMAGE_ATTACHMENT_TYPES = {
158
- "image/png",
159
- "image/jpeg",
160
- "image/jpg",
161
- "image/gif",
162
- "image/webp",
163
- }
164
- # Document MIME types that can be extracted as text
165
- EXTRACTABLE_DOCUMENT_TYPES = {
166
- "application/vnd.openxmlformats-officedocument.presentationml.presentation", # .pptx
167
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx
168
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx
169
- "application/vnd.ms-excel", # .xls
170
- "application/pdf", # .pdf
171
- }
@@ -33,8 +33,7 @@ dependencies = [
33
33
  "rich>=14.0.0",
34
34
  "ruff>=0.11.6",
35
35
  "streamlit>=1.44.1",
36
- "watchdog>=6.0.0",
37
- "markitdown[docx,pdf,pptx,xls,xlsx]>=0.1.3",
36
+ "watchdog>=6.0.0"
38
37
  ]
39
38
 
40
39
  [project.scripts]
@@ -1,175 +0,0 @@
1
- """Prompt building utilities for Pydantic AI agent, including file handling and context management."""
2
-
3
- import mimetypes
4
- from dataclasses import dataclass
5
- from pathlib import Path, PurePosixPath
6
- from typing import Optional
7
-
8
- from markitdown import MarkItDown
9
- from pydantic_ai import BinaryContent
10
-
11
- from aixtools.context import SessionIdTuple
12
- from aixtools.logging.logging_config import get_logger
13
- from aixtools.server import container_to_host_path
14
- from aixtools.utils.config import (
15
- EXTRACTABLE_DOCUMENT_TYPES,
16
- IMAGE_ATTACHMENT_TYPES,
17
- MAX_EXTRACTED_TEXT_SIZE,
18
- MAX_IMAGE_ATTACHMENT_SIZE,
19
- )
20
- from aixtools.utils.files import is_text_content
21
-
22
- logger = get_logger(__name__)
23
-
24
-
25
- @dataclass
26
- class FileExtractionResult:
27
- """Result of file content extraction.
28
-
29
- Attributes:
30
- content: Extracted file content (str for text/documents, BinaryContent for images, None on failure)
31
- success: True if file was successfully read or extracted, False on any failure
32
- error_message: Error description if extraction failed, None otherwise
33
- was_extracted: True if document extraction via markitdown was used successfully
34
- """
35
-
36
- content: str | BinaryContent | None
37
- success: bool
38
- error_message: str | None = None
39
- was_extracted: bool = False
40
-
41
-
42
- def should_be_included_into_context(
43
- file_content: BinaryContent | str | None,
44
- *,
45
- max_image_size_bytes: int = MAX_IMAGE_ATTACHMENT_SIZE,
46
- max_extracted_text_size_bytes: int = MAX_EXTRACTED_TEXT_SIZE,
47
- ) -> bool:
48
- """Check if file content should be included in model context based on type and size limits."""
49
- if file_content is None:
50
- return False
51
-
52
- # Handle extracted text (strings)
53
- if isinstance(file_content, str):
54
- text_size = len(file_content.encode("utf-8"))
55
- return text_size < max_extracted_text_size_bytes
56
-
57
- # Handle binary content (images only)
58
- if isinstance(file_content, BinaryContent):
59
- if file_content.media_type not in IMAGE_ATTACHMENT_TYPES:
60
- return False
61
- image_size = len(file_content.data)
62
- return image_size < max_image_size_bytes
63
-
64
- return False
65
-
66
-
67
- def file_to_binary_content(file_path: str | Path, mime_type: Optional[str] = None) -> FileExtractionResult:
68
- """Read file and extract text from documents (PDF, DOCX, XLSX, PPTX) using markitdown."""
69
- if not mime_type:
70
- mime_type, _ = mimetypes.guess_type(file_path)
71
- mime_type = mime_type or "application/octet-stream"
72
-
73
- # Extract text from supported document types using markitdown
74
- if mime_type in EXTRACTABLE_DOCUMENT_TYPES:
75
- try:
76
- markitdown = MarkItDown()
77
- result = markitdown.convert(str(file_path))
78
- return FileExtractionResult(
79
- content=result.text_content, success=True, error_message=None, was_extracted=True
80
- )
81
- except Exception as e: # pylint: disable=broad-exception-caught
82
- error_msg = f"Extraction failed: {type(e).__name__}: {str(e)}"
83
- logger.error("Document extraction failed for %s: %s", file_path, error_msg)
84
- return FileExtractionResult(content=None, success=False, error_message=error_msg)
85
-
86
- # Read the file data for non-document types
87
- try:
88
- with open(file_path, "rb") as f:
89
- data = f.read()
90
-
91
- # Return as string if it's text content
92
- if is_text_content(data, mime_type):
93
- return FileExtractionResult(content=data.decode("utf-8"), success=True)
94
-
95
- # Return as binary content for images and other binary files
96
- return FileExtractionResult(content=BinaryContent(data=data, media_type=mime_type), success=True)
97
- except Exception as e: # pylint: disable=broad-exception-caught
98
- error_msg = f"Failed to read file: {type(e).__name__}: {str(e)}"
99
- logger.error("File reading failed for %s: %s", file_path, error_msg)
100
- return FileExtractionResult(content=None, success=False, error_message=error_msg)
101
-
102
-
103
- def truncate_extracted_text(text: str, max_bytes: int = MAX_EXTRACTED_TEXT_SIZE) -> str:
104
- """Truncate text to max_bytes with warning prefix."""
105
- truncated_bytes = text.encode("utf-8")[:max_bytes]
106
- truncated_text = truncated_bytes.decode("utf-8", errors="ignore")
107
-
108
- total_chars = len(text)
109
- truncated_chars = len(truncated_text)
110
-
111
- return f"[TRUNCATED - showing first {truncated_chars} of {total_chars} characters]\n\n{truncated_text}"
112
-
113
-
114
- def build_user_input(
115
- session_tuple: SessionIdTuple,
116
- user_text: str,
117
- file_paths: list[Path],
118
- ) -> str | list[str | BinaryContent]:
119
- """Build user input for the Pydantic AI agent, including file attachments if provided."""
120
- if not file_paths:
121
- return user_text
122
-
123
- attachment_info_lines = []
124
- binary_attachments: list[str | BinaryContent] = []
125
-
126
- for workspace_path in file_paths:
127
- # Convert Path to PurePosixPath for container_to_host_path
128
- workspace_posix_path = PurePosixPath(workspace_path)
129
- host_path = container_to_host_path(workspace_posix_path, ctx=session_tuple)
130
-
131
- # Handle None return from container_to_host_path
132
- if host_path is None:
133
- attachment_info = (
134
- f"* {workspace_path.name} (path in workspace: {workspace_path}) -- conversion failed: invalid path"
135
- )
136
- attachment_info_lines.append(attachment_info)
137
- continue
138
-
139
- file_size = host_path.stat().st_size
140
- mime_type, _ = mimetypes.guess_type(host_path)
141
- mime_type = mime_type or "application/octet-stream"
142
-
143
- attachment_info = f"* {workspace_path.name} (file_size={file_size} bytes) (path in workspace: {workspace_path})"
144
- extraction_result = file_to_binary_content(host_path, mime_type)
145
-
146
- # Handle extraction failure - exclude from attachments
147
- if not extraction_result.success:
148
- attachment_info += f" -- extraction failed: {extraction_result.error_message}"
149
- attachment_info_lines.append(attachment_info)
150
- continue
151
-
152
- # Handle successful extraction
153
- if extraction_result.was_extracted:
154
- attachment_info += " -- extracted as text"
155
-
156
- # Check if content should be included in context
157
- if should_be_included_into_context(extraction_result.content) and extraction_result.content is not None:
158
- binary_attachments.append(extraction_result.content)
159
- attachment_info += f" -- provided to model context at index {len(binary_attachments) - 1}"
160
- elif (
161
- isinstance(extraction_result.content, str) and extraction_result.content and extraction_result.was_extracted
162
- ):
163
- # Truncate large extracted text and include with warning (only for extracted documents)
164
- truncated_content = truncate_extracted_text(extraction_result.content)
165
- binary_attachments.append(truncated_content)
166
- attachment_info += f" -- truncated and provided to model context at index {len(binary_attachments) - 1}"
167
- elif extraction_result.content is not None:
168
- # Content exists but excluded from context (e.g., images too large, non-extracted text)
169
- attachment_info += " -- too large for context"
170
-
171
- attachment_info_lines.append(attachment_info)
172
-
173
- full_prompt = user_text + "\nAttachments:\n" + "\n".join(attachment_info_lines)
174
-
175
- return [full_prompt] + binary_attachments
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes