retab 0.0.35__py3-none-any.whl

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.
Files changed (111) hide show
  1. retab-0.0.35.dist-info/METADATA +417 -0
  2. retab-0.0.35.dist-info/RECORD +111 -0
  3. retab-0.0.35.dist-info/WHEEL +5 -0
  4. retab-0.0.35.dist-info/top_level.txt +1 -0
  5. uiform/__init__.py +4 -0
  6. uiform/_resource.py +28 -0
  7. uiform/_utils/__init__.py +0 -0
  8. uiform/_utils/ai_models.py +100 -0
  9. uiform/_utils/benchmarking copy.py +588 -0
  10. uiform/_utils/benchmarking.py +485 -0
  11. uiform/_utils/chat.py +332 -0
  12. uiform/_utils/display.py +443 -0
  13. uiform/_utils/json_schema.py +2161 -0
  14. uiform/_utils/mime.py +168 -0
  15. uiform/_utils/responses.py +163 -0
  16. uiform/_utils/stream_context_managers.py +52 -0
  17. uiform/_utils/usage/__init__.py +0 -0
  18. uiform/_utils/usage/usage.py +300 -0
  19. uiform/client.py +701 -0
  20. uiform/py.typed +0 -0
  21. uiform/resources/__init__.py +0 -0
  22. uiform/resources/consensus/__init__.py +3 -0
  23. uiform/resources/consensus/client.py +114 -0
  24. uiform/resources/consensus/completions.py +252 -0
  25. uiform/resources/consensus/completions_stream.py +278 -0
  26. uiform/resources/consensus/responses.py +325 -0
  27. uiform/resources/consensus/responses_stream.py +373 -0
  28. uiform/resources/deployments/__init__.py +9 -0
  29. uiform/resources/deployments/client.py +78 -0
  30. uiform/resources/deployments/endpoints.py +322 -0
  31. uiform/resources/deployments/links.py +452 -0
  32. uiform/resources/deployments/logs.py +211 -0
  33. uiform/resources/deployments/mailboxes.py +496 -0
  34. uiform/resources/deployments/outlook.py +531 -0
  35. uiform/resources/deployments/tests.py +158 -0
  36. uiform/resources/documents/__init__.py +3 -0
  37. uiform/resources/documents/client.py +255 -0
  38. uiform/resources/documents/extractions.py +441 -0
  39. uiform/resources/evals.py +812 -0
  40. uiform/resources/files.py +24 -0
  41. uiform/resources/finetuning.py +62 -0
  42. uiform/resources/jsonlUtils.py +1046 -0
  43. uiform/resources/models.py +45 -0
  44. uiform/resources/openai_example.py +22 -0
  45. uiform/resources/processors/__init__.py +3 -0
  46. uiform/resources/processors/automations/__init__.py +9 -0
  47. uiform/resources/processors/automations/client.py +78 -0
  48. uiform/resources/processors/automations/endpoints.py +317 -0
  49. uiform/resources/processors/automations/links.py +356 -0
  50. uiform/resources/processors/automations/logs.py +211 -0
  51. uiform/resources/processors/automations/mailboxes.py +435 -0
  52. uiform/resources/processors/automations/outlook.py +444 -0
  53. uiform/resources/processors/automations/tests.py +158 -0
  54. uiform/resources/processors/client.py +474 -0
  55. uiform/resources/prompt_optimization.py +76 -0
  56. uiform/resources/schemas.py +369 -0
  57. uiform/resources/secrets/__init__.py +9 -0
  58. uiform/resources/secrets/client.py +20 -0
  59. uiform/resources/secrets/external_api_keys.py +109 -0
  60. uiform/resources/secrets/webhook.py +62 -0
  61. uiform/resources/usage.py +271 -0
  62. uiform/types/__init__.py +0 -0
  63. uiform/types/ai_models.py +645 -0
  64. uiform/types/automations/__init__.py +0 -0
  65. uiform/types/automations/cron.py +58 -0
  66. uiform/types/automations/endpoints.py +21 -0
  67. uiform/types/automations/links.py +28 -0
  68. uiform/types/automations/mailboxes.py +60 -0
  69. uiform/types/automations/outlook.py +68 -0
  70. uiform/types/automations/webhooks.py +21 -0
  71. uiform/types/chat.py +8 -0
  72. uiform/types/completions.py +93 -0
  73. uiform/types/consensus.py +10 -0
  74. uiform/types/db/__init__.py +0 -0
  75. uiform/types/db/annotations.py +24 -0
  76. uiform/types/db/files.py +36 -0
  77. uiform/types/deployments/__init__.py +0 -0
  78. uiform/types/deployments/cron.py +59 -0
  79. uiform/types/deployments/endpoints.py +28 -0
  80. uiform/types/deployments/links.py +36 -0
  81. uiform/types/deployments/mailboxes.py +67 -0
  82. uiform/types/deployments/outlook.py +76 -0
  83. uiform/types/deployments/webhooks.py +21 -0
  84. uiform/types/documents/__init__.py +0 -0
  85. uiform/types/documents/correct_orientation.py +13 -0
  86. uiform/types/documents/create_messages.py +226 -0
  87. uiform/types/documents/extractions.py +297 -0
  88. uiform/types/evals.py +207 -0
  89. uiform/types/events.py +76 -0
  90. uiform/types/extractions.py +85 -0
  91. uiform/types/jobs/__init__.py +0 -0
  92. uiform/types/jobs/base.py +150 -0
  93. uiform/types/jobs/batch_annotation.py +22 -0
  94. uiform/types/jobs/evaluation.py +133 -0
  95. uiform/types/jobs/finetune.py +6 -0
  96. uiform/types/jobs/prompt_optimization.py +41 -0
  97. uiform/types/jobs/webcrawl.py +6 -0
  98. uiform/types/logs.py +231 -0
  99. uiform/types/mime.py +257 -0
  100. uiform/types/modalities.py +68 -0
  101. uiform/types/pagination.py +6 -0
  102. uiform/types/schemas/__init__.py +0 -0
  103. uiform/types/schemas/enhance.py +53 -0
  104. uiform/types/schemas/evaluate.py +55 -0
  105. uiform/types/schemas/generate.py +32 -0
  106. uiform/types/schemas/layout.py +58 -0
  107. uiform/types/schemas/object.py +631 -0
  108. uiform/types/schemas/templates.py +107 -0
  109. uiform/types/secrets/__init__.py +0 -0
  110. uiform/types/secrets/external_api_keys.py +22 -0
  111. uiform/types/standards.py +39 -0
@@ -0,0 +1,76 @@
1
+ import copy
2
+ import datetime
3
+ import json
4
+ import os
5
+ import re
6
+ from typing import Any, ClassVar, Dict, List, Literal, Optional
7
+
8
+ import nanoid # type: ignore
9
+ from openai.types.chat.chat_completion_reasoning_effort import ChatCompletionReasoningEffort
10
+ from pydantic import BaseModel, EmailStr, Field, HttpUrl, computed_field, field_serializer, field_validator, model_validator
11
+ from pydantic_core import Url
12
+
13
+ from ..._utils.json_schema import clean_schema, convert_schema_to_layout
14
+ from ..._utils.mime import generate_blake2b_hash_from_string
15
+ from ..logs import AutomationConfig, UpdateAutomationRequest
16
+ from ..modalities import Modality
17
+ from ..pagination import ListMetadata
18
+
19
+ domain_pattern = re.compile(r"^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$")
20
+
21
+
22
+ class AutomationLevel(BaseModel):
23
+ distance_threshold: float = Field(default=0.9, description="Distance threshold for the automation")
24
+ score_threshold: float = Field(default=0.9, description="Score threshold for the automation")
25
+
26
+
27
+ class MatchParams(BaseModel):
28
+ endpoint: str = Field(..., description="Endpoint for matching parameters")
29
+ headers: Dict[str, str] = Field(..., description="Headers for the request")
30
+ path: str = Field(..., description="Path for matching parameters")
31
+
32
+
33
+ class FetchParams(BaseModel):
34
+ endpoint: str = Field(..., description="Endpoint for fetching parameters")
35
+ headers: Dict[str, str] = Field(..., description="Headers for the request")
36
+ name: str = Field(..., description="Name of the fetch parameter")
37
+
38
+
39
+ class Outlook(AutomationConfig):
40
+ object: Literal['deployment.outlook'] = "deployment.outlook"
41
+ id: str = Field(default_factory=lambda: "outlook_" + nanoid.generate(), description="Unique identifier for the outlook")
42
+
43
+ authorized_domains: list[str] = Field(default_factory=list, description="List of authorized domains to receive the emails from")
44
+ authorized_emails: List[EmailStr] = Field(default_factory=list, description="List of emails to access the link")
45
+
46
+ layout_schema: Optional[dict[str, Any]] = Field(default=None, description="Layout schema format used to display the data")
47
+
48
+ # Optional Fields for data integration
49
+ match_params: List[MatchParams] = Field(default_factory=list, description="List of match parameters for the outlook automation")
50
+ fetch_params: List[FetchParams] = Field(default_factory=list, description="List of fetch parameters for the outlook automation")
51
+
52
+ @model_validator(mode='before')
53
+ @classmethod
54
+ def compute_layout_schema(cls, values: dict[str, Any]) -> dict[str, Any]:
55
+ if values.get('layout_schema') is None:
56
+ values['layout_schema'] = convert_schema_to_layout(values['json_schema'])
57
+ return values
58
+
59
+ class ListOutlooks(BaseModel):
60
+ data: list[Outlook]
61
+ list_metadata: ListMetadata
62
+
63
+
64
+ # Inherits from the methods of UpdateAutomationRequest
65
+ class UpdateOutlookRequest(UpdateAutomationRequest):
66
+ authorized_domains: Optional[list[str]] = None
67
+ authorized_emails: Optional[List[EmailStr]] = None
68
+
69
+ match_params: Optional[List[MatchParams]] = None
70
+ fetch_params: Optional[List[FetchParams]] = None
71
+
72
+ layout_schema: Optional[dict[str, Any]] = None
73
+
74
+ @field_validator("authorized_emails", mode="before")
75
+ def normalize_authorized_emails(cls, emails: Optional[List[str]]) -> Optional[List[str]]:
76
+ return [email.strip().lower() for email in emails] if emails else None
@@ -0,0 +1,21 @@
1
+ from typing import Any, Optional
2
+
3
+ from pydantic import BaseModel, EmailStr
4
+
5
+ from uiform.types.documents.extractions import UiParsedChatCompletion
6
+
7
+ from ..mime import BaseMIMEData, MIMEData
8
+
9
+
10
+ class WebhookRequest(BaseModel):
11
+ completion: UiParsedChatCompletion
12
+ user: Optional[EmailStr] = None
13
+ file_payload: MIMEData
14
+ metadata: Optional[dict[str, Any]] = None
15
+
16
+
17
+ class BaseWebhookRequest(BaseModel):
18
+ completion: UiParsedChatCompletion
19
+ user: Optional[EmailStr] = None
20
+ file_payload: BaseMIMEData
21
+ metadata: Optional[dict[str, Any]] = None
File without changes
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+ from ..mime import MIMEData
4
+
5
+
6
+ class DocumentTransformRequest(BaseModel):
7
+ document: MIMEData
8
+ """The document to load."""
9
+
10
+
11
+ class DocumentTransformResponse(BaseModel):
12
+ document: MIMEData
13
+ """The document to load."""
@@ -0,0 +1,226 @@
1
+ import base64
2
+ from io import BytesIO
3
+ from typing import List, Literal, Dict, Union
4
+
5
+ import PIL.Image
6
+ import requests
7
+ from anthropic.types.message_param import MessageParam
8
+ from google.genai.types import ContentUnionDict # type: ignore
9
+ from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
10
+ from openai.types.responses.response_input_param import ResponseInputItemParam
11
+ from pydantic import BaseModel, Field, computed_field
12
+
13
+ from ..._utils.chat import convert_to_anthropic_format, convert_to_google_genai_format, str_messages
14
+ from ..._utils.chat import convert_to_openai_format as convert_to_openai_completions_api_format
15
+ from ..._utils.responses import convert_to_openai_format as convert_to_openai_responses_api_format
16
+ from ..._utils.display import count_text_tokens, count_image_tokens
17
+ from ..chat import ChatCompletionUiformMessage
18
+ from ..mime import MIMEData
19
+ from ..modalities import Modality
20
+
21
+ MediaType = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
22
+
23
+
24
+ class TokenCount(BaseModel):
25
+ total_tokens: int = 0
26
+ developer_tokens: int = 0
27
+ user_tokens: int = 0
28
+
29
+ class DocumentCreateMessageRequest(BaseModel):
30
+ document: MIMEData = Field(description="The document to load.")
31
+ modality: Modality = Field(description="The modality of the document to load.")
32
+ image_resolution_dpi: int = Field(default=96, description="Resolution of the image sent to the LLM")
33
+ browser_canvas: Literal['A3', 'A4', 'A5'] = Field(default='A4', description="Sets the size of the browser canvas for rendering documents in browser-based processing. Choose a size that matches the document type.")
34
+
35
+ from typing import Any
36
+ class DocumentCreateInputRequest(DocumentCreateMessageRequest):
37
+ json_schema: dict[str, Any] = Field(description="The json schema to use for the document.")
38
+
39
+
40
+
41
+
42
+ class DocumentMessage(BaseModel):
43
+ id: str = Field(description="A unique identifier for the document loading.")
44
+ object: Literal["document_message"] = Field(default="document_message", description="The type of object being loaded.")
45
+ messages: List[ChatCompletionUiformMessage] = Field(description="A list of messages containing the document content and metadata.")
46
+ created: int = Field(description="The Unix timestamp (in seconds) of when the document was loaded.")
47
+ modality: Modality = Field(description="The modality of the document to load.")
48
+
49
+ @computed_field
50
+ def token_count(self) -> TokenCount:
51
+ """Returns the token count for the document message.
52
+
53
+ This property calculates token usage based on both text and image content
54
+ in the messages using the token counting utilities.
55
+
56
+ Returns:
57
+ TokenCount: A Pydantic model with total, user, and developer token counts.
58
+ """
59
+ total_tokens = 0
60
+ user_tokens = 0
61
+ developer_tokens = 0
62
+
63
+ for msg in self.messages:
64
+ role = msg.get("role", "user")
65
+ msg_tokens = 0
66
+
67
+ if isinstance(msg["content"], str):
68
+ msg_tokens = count_text_tokens(msg["content"])
69
+ elif isinstance(msg["content"], list):
70
+ for content_item in msg["content"]:
71
+ if isinstance(content_item, str):
72
+ msg_tokens += count_text_tokens(content_item)
73
+ elif isinstance(content_item, dict):
74
+ item_type = content_item.get("type")
75
+
76
+ if item_type == "text" and "text" in content_item:
77
+ msg_tokens += count_text_tokens(content_item["text"])
78
+
79
+ elif item_type == "image_url" and "image_url" in content_item:
80
+ image_url = content_item["image_url"]["url"]
81
+ detail = content_item["image_url"].get("detail", "high")
82
+ msg_tokens += count_image_tokens(image_url, detail)
83
+
84
+ # Update total tokens
85
+ total_tokens += msg_tokens
86
+
87
+ # Update role-specific counts
88
+ assert role in ["user", "developer"], f"Invalid role: {role}"
89
+ if role == "user":
90
+ user_tokens += msg_tokens
91
+ elif role == "developer":
92
+ developer_tokens += msg_tokens
93
+
94
+ return TokenCount(
95
+ total_tokens=total_tokens,
96
+ user_tokens=user_tokens,
97
+ developer_tokens=developer_tokens
98
+ )
99
+
100
+ @property
101
+ def items(self) -> list[str | PIL.Image.Image]:
102
+ """Returns the document contents as a list of strings and images.
103
+
104
+ This property processes the message content and converts it into a list of either
105
+ text strings or PIL Image objects. It handles various content types including:
106
+ - Plain text
107
+ - Base64 encoded images
108
+ - Remote image URLs
109
+ - Audio data (represented as truncated string)
110
+
111
+ Returns:
112
+ list[str | PIL.Image.Image]: A list containing either strings for text content
113
+ or PIL.Image.Image objects for image content. Failed image loads will
114
+ return their URLs as strings instead.
115
+ """
116
+ results: list[str | PIL.Image.Image] = []
117
+
118
+ for msg in self.messages:
119
+ if isinstance(msg["content"], str):
120
+ results.append(msg["content"])
121
+ continue
122
+ assert isinstance(msg["content"], list), "content must be a list or a string"
123
+ for content_item in msg["content"]:
124
+ if isinstance(content_item, str):
125
+ results.append(content_item)
126
+ else:
127
+ item_type = content_item.get("type")
128
+ # If item is an image
129
+ if item_type == "image_url":
130
+ assert "image_url" in content_item, "image_url is required in ChatCompletionContentPartImageParam"
131
+ image_data_url = content_item["image_url"]["url"] # type: ignore
132
+
133
+ # 1) Base64 inline data
134
+ if image_data_url.startswith("data:image/"):
135
+ try:
136
+ prefix, base64_part = image_data_url.split(",", 1)
137
+ img_bytes = base64.b64decode(base64_part)
138
+ img = PIL.Image.open(BytesIO(img_bytes))
139
+ results.append(img)
140
+ except Exception as e:
141
+ print(f"Error decoding base64 data:\n {e}")
142
+ results.append(image_data_url)
143
+
144
+ # 2) Otherwise, assume it's a remote URL
145
+ else:
146
+ try:
147
+ response = requests.get(image_data_url)
148
+ response.raise_for_status() # raises HTTPError if not 200
149
+ img = PIL.Image.open(BytesIO(response.content))
150
+ results.append(img)
151
+ except Exception as e:
152
+ # Here, log or print the actual error
153
+ print(f"Could not download image from {image_data_url}:\n {e}")
154
+ results.append(image_data_url)
155
+
156
+ # If item is text (or other types)
157
+ elif item_type == "text":
158
+ text_value = content_item.get("text", "")
159
+ assert isinstance(text_value, str), "text is required in ChatCompletionContentPartTextParam"
160
+ results.append(text_value)
161
+
162
+ elif item_type == "input_audio":
163
+ # Handle audio input content
164
+ if "input_audio" in content_item:
165
+ audio_data = content_item["input_audio"]["data"] # type: ignore
166
+ results.append(f"Audio data: {audio_data[:100]}...") # Truncate long audio data
167
+
168
+ else:
169
+ # Fallback for unrecognized item types
170
+ results.append(f"Unrecognized type: {item_type}")
171
+
172
+ return results
173
+
174
+ @property
175
+ def openai_messages(self) -> list[ChatCompletionMessageParam]:
176
+ """Returns the messages formatted for OpenAI's API.
177
+
178
+ Converts the internal message format to OpenAI's expected format for
179
+ chat completions.
180
+
181
+ Returns:
182
+ list[ChatCompletionMessageParam]: Messages formatted for OpenAI's chat completion API.
183
+ """
184
+ return convert_to_openai_completions_api_format(self.messages)
185
+
186
+ @property
187
+ def openai_responses_input(self) -> list[ResponseInputItemParam]:
188
+ """Returns the messages formatted for OpenAI's Responses API.
189
+
190
+ Converts the internal message format to OpenAI's expected format for
191
+ responses.
192
+
193
+ Returns:
194
+ list[ResponseInputItemParam]: Messages formatted for OpenAI's responses API.
195
+ """
196
+ return convert_to_openai_responses_api_format(self.messages)
197
+
198
+ @property
199
+ def anthropic_messages(self) -> list[MessageParam]:
200
+ """Returns the messages formatted for Anthropic's Claude API.
201
+
202
+ Converts the internal message format to Claude's expected format,
203
+ handling text, images, and other content types appropriately.
204
+
205
+ Returns:
206
+ list[MessageParam]: Messages formatted for Claude's API.
207
+ """
208
+ return convert_to_anthropic_format(self.messages)[1]
209
+
210
+ @property
211
+ def gemini_messages(self) -> list[ContentUnionDict]:
212
+ """Returns the messages formatted for Google's Gemini API.
213
+
214
+ Converts the internal message format to Gemini's expected format,
215
+ handling various content types including text and images.
216
+
217
+ Returns:
218
+ list[PartDict]: Messages formatted for Gemini's API.
219
+ """
220
+ return convert_to_google_genai_format(self.messages)[1]
221
+
222
+ def __str__(self) -> str:
223
+ return f"DocumentMessage(id={self.id}, object={self.object}, created={self.created}, messages={str_messages(self.messages)}, modality={self.modality})"
224
+
225
+ def __repr__(self) -> str:
226
+ return f"DocumentMessage(id={self.id}, object={self.object}, created={self.created}, messages={str_messages(self.messages)}, modality={self.modality})"
@@ -0,0 +1,297 @@
1
+ import base64
2
+ import datetime
3
+ from typing import Any, Literal, Optional
4
+
5
+ from anthropic.types.message import Message
6
+ from anthropic.types.message_param import MessageParam
7
+ from openai.types.chat import ChatCompletionMessageParam
8
+ from openai.types.chat.chat_completion import ChatCompletion
9
+ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
10
+ from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
11
+ from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChoiceDeltaChunk
12
+ from openai.types.chat.chat_completion_reasoning_effort import ChatCompletionReasoningEffort
13
+ from openai.types.chat.parsed_chat_completion import ParsedChatCompletion, ParsedChoice
14
+ from openai.types.responses.response import Response
15
+ from openai.types.responses.response_input_param import ResponseInputItemParam
16
+ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, computed_field, field_validator, model_validator
17
+
18
+ from ..._utils.usage.usage import compute_cost_from_model, compute_cost_from_model_with_breakdown, CostBreakdown
19
+
20
+ from ..._utils.ai_models import find_provider_from_model
21
+ from ..ai_models import AIProvider, Amount, get_model_card
22
+ from ..chat import ChatCompletionUiformMessage
23
+ from ..mime import MIMEData
24
+ from ..modalities import Modality
25
+ from ..standards import ErrorDetail, StreamingBaseModel
26
+
27
+
28
+ class DocumentExtractRequest(BaseModel):
29
+ model_config = ConfigDict(arbitrary_types_allowed=True)
30
+
31
+ document: MIMEData = Field(..., description="Document to be analyzed")
32
+ modality: Modality
33
+ image_resolution_dpi: int = Field(default=96, description="Resolution of the image sent to the LLM")
34
+ browser_canvas: Literal['A3', 'A4', 'A5'] = Field(
35
+ default='A4', description="Sets the size of the browser canvas for rendering documents in browser-based processing. Choose a size that matches the document type."
36
+ )
37
+ model: str = Field(..., description="Model used for chat completion")
38
+ json_schema: dict[str, Any] = Field(..., description="JSON schema format used to validate the output data.")
39
+ temperature: float = Field(default=0.0, description="Temperature for sampling. If not provided, the default temperature for the model will be used.", examples=[0.0])
40
+ reasoning_effort: ChatCompletionReasoningEffort = Field(
41
+ default="medium", description="The effort level for the model to reason about the input data. If not provided, the default reasoning effort for the model will be used."
42
+ )
43
+ n_consensus: int = Field(default=1, description="Number of consensus models to use for extraction. If greater than 1 the temperature cannot be 0.")
44
+ # Regular fields
45
+ stream: bool = Field(default=False, description="If true, the extraction will be streamed to the user using the active WebSocket connection")
46
+ seed: int | None = Field(default=None, description="Seed for the random number generator. If not provided, a random seed will be generated.", examples=[None])
47
+ store: bool = Field(default=True, description="If true, the extraction will be stored in the database")
48
+ need_validation: bool = Field(default=False, description="If true, the extraction will be validated against the schema")
49
+
50
+ # Add a model validator that rejects n_consensus > 1 if temperature is 0
51
+ @field_validator("n_consensus")
52
+ def check_n_consensus(cls, v: int, info: ValidationInfo) -> int:
53
+ if v > 1 and info.data.get("temperature") == 0:
54
+ raise ValueError("n_consensus greater than 1 but temperature is 0")
55
+ return v
56
+
57
+
58
+ class ConsensusModel(BaseModel):
59
+ model: str = Field(description="Model name")
60
+ temperature: float = Field(default=0.0, description="Temperature for consensus")
61
+ reasoning_effort: ChatCompletionReasoningEffort = Field(
62
+ default="medium", description="The effort level for the model to reason about the input data. If not provided, the default reasoning effort for the model will be used."
63
+ )
64
+
65
+
66
+ # For location of fields in the document (OCR)
67
+ class FieldLocation(BaseModel):
68
+ label: str = Field(..., description="The label of the field")
69
+ value: str = Field(..., description="The extracted value of the field")
70
+ quote: str = Field(..., description="The quote of the field (verbatim from the document)")
71
+ file_id: str | None = Field(default=None, description="The ID of the file")
72
+ page: int | None = Field(default=None, description="The page number of the field (1-indexed)")
73
+ bboxes_normalized: list[tuple[float, float, float, float]] | None = Field(default=None, description="The normalized bounding boxes of the field")
74
+ score: float | None = Field(default=None, description="The score of the field")
75
+ match_level: Literal["token", "line", "block"] | None = Field(default=None, description="The level of the match (token, line, block)")
76
+
77
+
78
+ class UiParsedChoice(ParsedChoice):
79
+ # Adaptable ParsedChoice that allows None for the finish_reason
80
+ finish_reason: Literal["stop", "length", "tool_calls", "content_filter", "function_call"] | None = None # type: ignore
81
+ field_locations: dict[str, list[FieldLocation]] | None = Field(default=None, description="The locations of the fields in the document, if available")
82
+ key_mapping: dict[str, Optional[str]] | None = Field(default=None, description="Mapping of consensus keys to original model keys")
83
+
84
+
85
+ LikelihoodsSource = Literal["consensus", "log_probs"]
86
+
87
+
88
+ class UiParsedChatCompletion(ParsedChatCompletion):
89
+ extraction_id: str | None = None
90
+ choices: list[UiParsedChoice]
91
+ # Additional metadata fields (UIForm)
92
+ likelihoods: Optional[dict[str, Any]] = Field(
93
+ default=None, description="Object defining the uncertainties of the fields extracted when using consensus. Follows the same structure as the extraction object."
94
+ )
95
+ schema_validation_error: ErrorDetail | None = None
96
+ # Timestamps
97
+ request_at: datetime.datetime | None = Field(default=None, description="Timestamp of the request")
98
+ first_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the first token of the document. If non-streaming, set to last_token_at")
99
+ last_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the last token of the document")
100
+
101
+ @computed_field
102
+ @property
103
+ def api_cost(self) -> Optional[Amount]:
104
+ if self.usage:
105
+ try:
106
+ cost = compute_cost_from_model(self.model, self.usage)
107
+ return cost
108
+ except Exception as e:
109
+ print(f"Error computing cost: {e}")
110
+ return None
111
+ return None
112
+
113
+
114
+ class UiResponse(Response):
115
+ extraction_id: str | None = None
116
+ # Additional metadata fields (UIForm)
117
+ likelihoods: Optional[dict[str, Any]] = Field(
118
+ default=None, description="Object defining the uncertainties of the fields extracted when using consensus. Follows the same structure as the extraction object."
119
+ )
120
+ schema_validation_error: ErrorDetail | None = None
121
+ # Timestamps
122
+ request_at: datetime.datetime | None = Field(default=None, description="Timestamp of the request")
123
+ first_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the first token of the document. If non-streaming, set to last_token_at")
124
+ last_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the last token of the document")
125
+
126
+
127
+ class LogExtractionRequest(BaseModel):
128
+ messages: list[ChatCompletionUiformMessage] | None = None # TODO: compatibility with Anthropic
129
+ openai_messages: list[ChatCompletionMessageParam] | None = None
130
+ openai_responses_input: list[ResponseInputItemParam] | None = None
131
+ anthropic_messages: list[MessageParam] | None = None
132
+ anthropic_system_prompt: str | None = None
133
+ document: MIMEData = Field(
134
+ default=MIMEData(
135
+ filename="dummy.txt",
136
+ # url is a base64 encoded string with the mime type and the content. For the dummy one we will send a .txt file with the text "No document provided"
137
+ url="data:text/plain;base64," + base64.b64encode(b"No document provided").decode("utf-8"),
138
+ ),
139
+ description="Document analyzed, if not provided a dummy one will be created with the text 'No document provided'",
140
+ )
141
+ completion: dict | UiParsedChatCompletion | Message | ParsedChatCompletion | ChatCompletion | None = None
142
+ openai_responses_output: Response | None = None
143
+ json_schema: dict[str, Any]
144
+ model: str
145
+ temperature: float
146
+
147
+ # Validate that at least one of the messages, openai_messages, anthropic_messages is provided using model_validator
148
+ @model_validator(mode="before")
149
+ def validation(cls, data: Any) -> Any:
150
+ messages_candidates = [data.get("messages"), data.get("openai_messages"), data.get("anthropic_messages"), data.get("openai_responses_input")]
151
+ messages_candidates = [candidate for candidate in messages_candidates if candidate is not None]
152
+ if len(messages_candidates) != 1:
153
+ raise ValueError("Exactly one of the messages, openai_messages, anthropic_messages, openai_responses_input must be provided")
154
+
155
+ # Validate that if anthropic_messages is provided, anthropic_system_prompt is also provided
156
+ if data.get("anthropic_messages") is not None and data.get("anthropic_system_prompt") is None:
157
+ raise ValueError("anthropic_system_prompt must be provided if anthropic_messages is provided")
158
+
159
+ completion_candidates = [data.get("completion"), data.get("openai_responses_output")]
160
+ completion_candidates = [candidate for candidate in completion_candidates if candidate is not None]
161
+ if len(completion_candidates) != 1:
162
+ raise ValueError("Exactly one of completion, openai_responses_output must be provided")
163
+
164
+ return data
165
+
166
+
167
+ class LogExtractionResponse(BaseModel):
168
+ extraction_id: str | None = None # None only in case of error
169
+ status: Literal["success", "error"]
170
+ error_message: str | None = None
171
+
172
+
173
+ # DocumentExtractResponse = UiParsedChatCompletion
174
+
175
+
176
+ ###### I'll place here for now -- New Streaming API
177
+
178
+
179
+ # We build from the openai.types.chat.chat_completion_chunk.ChatCompletionChunk adding just two three additional fields:
180
+ # - is_valid_json: list[bool] # Whether the total accumulated content is a valid JSON
181
+ # - likelihoods: dict[str, float] # The delta of the flattened likelihoods (to be merged with the cumulated likelihoods)
182
+ # - schema_validation_error: ErrorDetail | None = None # The error in the schema validation of the total accumulated content
183
+
184
+
185
+ class UiParsedChoiceDeltaChunk(ChoiceDeltaChunk):
186
+ flat_likelihoods: dict[str, float] = {}
187
+ flat_parsed: dict[str, Any] = {}
188
+ flat_deleted_keys: list[str] = []
189
+ field_locations: dict[str, list[FieldLocation]] | None = Field(default=None, description="The locations of the fields in the document, if available")
190
+ is_valid_json: bool = False
191
+ key_mapping: dict[str, Optional[str]] | None = Field(default=None, description="Mapping of consensus keys to original model keys")
192
+
193
+
194
+ class UiParsedChoiceChunk(ChoiceChunk):
195
+ delta: UiParsedChoiceDeltaChunk
196
+
197
+
198
+ class UiParsedChatCompletionChunk(StreamingBaseModel, ChatCompletionChunk):
199
+ extraction_id: str | None = None
200
+ choices: list[UiParsedChoiceChunk]
201
+ schema_validation_error: ErrorDetail | None = None
202
+ # Timestamps
203
+ request_at: datetime.datetime | None = Field(default=None, description="Timestamp of the request")
204
+ first_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the first token of the document. If non-streaming, set to last_token_at")
205
+ last_token_at: datetime.datetime | None = Field(default=None, description="Timestamp of the last token of the document")
206
+
207
+ @computed_field
208
+ @property
209
+ def api_cost(self) -> Optional[Amount]:
210
+ if self.usage:
211
+ try:
212
+ cost = compute_cost_from_model(self.model, self.usage)
213
+ return cost
214
+ except Exception as e:
215
+ print(f"Error computing cost: {e}")
216
+ return None
217
+ return None
218
+
219
+ @computed_field # type: ignore
220
+ @property
221
+ def cost_breakdown(self) -> Optional[CostBreakdown]:
222
+ if self.usage:
223
+ try:
224
+ cost = compute_cost_from_model_with_breakdown(self.model, self.usage)
225
+ return cost
226
+ except Exception as e:
227
+ print(f"Error computing cost: {e}")
228
+ return None
229
+ return None
230
+
231
+ def chunk_accumulator(self, previous_cumulated_chunk: "UiParsedChatCompletionChunk | None" = None) -> "UiParsedChatCompletionChunk":
232
+ """
233
+ Accumulate the chunk into the state, returning a new UiParsedChatCompletionChunk with the accumulated content that could be yielded alone to generate the same state.
234
+ """
235
+
236
+ def safe_get_delta(chnk: "UiParsedChatCompletionChunk | None", index: int) -> UiParsedChoiceDeltaChunk:
237
+ if chnk is not None and index < len(chnk.choices):
238
+ return chnk.choices[index].delta
239
+ else:
240
+ return UiParsedChoiceDeltaChunk(
241
+ content="",
242
+ flat_parsed={},
243
+ flat_likelihoods={},
244
+ is_valid_json=False,
245
+ )
246
+
247
+ max_choices = max(len(self.choices), len(previous_cumulated_chunk.choices)) if previous_cumulated_chunk is not None else len(self.choices)
248
+
249
+ # Get the current chunk missing content, flat_deleted_keys and is_valid_json
250
+ acc_flat_deleted_keys = [safe_get_delta(self, i).flat_deleted_keys for i in range(max_choices)]
251
+ acc_is_valid_json = [safe_get_delta(self, i).is_valid_json for i in range(max_choices)]
252
+ acc_field_locations = [safe_get_delta(self, i).field_locations for i in range(max_choices)] # This is only present in the last chunk.
253
+ # Delete from previous_cumulated_chunk.choices[i].delta.flat_parsed the keys that are in safe_get_delta(self, i).flat_deleted_keys
254
+ for i in range(max_choices):
255
+ previous_delta = safe_get_delta(previous_cumulated_chunk, i)
256
+ current_delta = safe_get_delta(self, i)
257
+ for deleted_key in current_delta.flat_deleted_keys:
258
+ previous_delta.flat_parsed.pop(deleted_key, None)
259
+ previous_delta.flat_likelihoods.pop(deleted_key, None)
260
+ # Accumulate the flat_parsed and flat_likelihoods
261
+ acc_flat_parsed = [safe_get_delta(previous_cumulated_chunk, i).flat_parsed | safe_get_delta(self, i).flat_parsed for i in range(max_choices)]
262
+ acc_flat_likelihoods = [safe_get_delta(previous_cumulated_chunk, i).flat_likelihoods | safe_get_delta(self, i).flat_likelihoods for i in range(max_choices)]
263
+ acc_key_mapping = [safe_get_delta(previous_cumulated_chunk, i).key_mapping or safe_get_delta(self, i).key_mapping for i in range(max_choices)]
264
+
265
+ acc_content = [(safe_get_delta(previous_cumulated_chunk, i).content or "") + (safe_get_delta(self, i).content or "") for i in range(max_choices)]
266
+ usage = self.usage
267
+ first_token_at = self.first_token_at
268
+ last_token_at = self.last_token_at
269
+ request_at = self.request_at
270
+
271
+ return UiParsedChatCompletionChunk(
272
+ extraction_id=self.extraction_id,
273
+ id=self.id,
274
+ created=self.created,
275
+ model=self.model,
276
+ object=self.object,
277
+ usage=usage,
278
+ choices=[
279
+ UiParsedChoiceChunk(
280
+ delta=UiParsedChoiceDeltaChunk(
281
+ content=acc_content[i],
282
+ flat_parsed=acc_flat_parsed[i],
283
+ flat_likelihoods=acc_flat_likelihoods[i],
284
+ flat_deleted_keys=acc_flat_deleted_keys[i],
285
+ field_locations=acc_field_locations[i],
286
+ is_valid_json=acc_is_valid_json[i],
287
+ key_mapping=acc_key_mapping[i],
288
+ ),
289
+ index=i,
290
+ )
291
+ for i in range(max_choices)
292
+ ],
293
+ schema_validation_error=self.schema_validation_error,
294
+ request_at=request_at,
295
+ first_token_at=first_token_at,
296
+ last_token_at=last_token_at,
297
+ )