agno 2.0.0rc2__py3-none-any.whl → 2.3.0__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.
- agno/agent/agent.py +6009 -2874
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +595 -187
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +3 -0
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +339 -266
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +1011 -566
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +110 -37
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +143 -4
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +60 -6
- agno/models/openai/chat.py +102 -43
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +81 -5
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -175
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +266 -112
- agno/run/base.py +53 -24
- agno/run/team.py +252 -111
- agno/run/workflow.py +156 -45
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1692
- agno/tools/brightdata.py +3 -3
- agno/tools/cartesia.py +3 -5
- agno/tools/dalle.py +9 -8
- agno/tools/decorator.py +4 -2
- agno/tools/desi_vocal.py +2 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +20 -13
- agno/tools/eleven_labs.py +26 -28
- agno/tools/exa.py +21 -16
- agno/tools/fal.py +4 -4
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +257 -37
- agno/tools/giphy.py +2 -2
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/lumalab.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/azure_openai.py +2 -2
- agno/tools/models/gemini.py +3 -3
- agno/tools/models/groq.py +3 -5
- agno/tools/models/nebius.py +7 -7
- agno/tools/models_labs.py +25 -15
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +4 -9
- agno/tools/opencv.py +3 -3
- agno/tools/parallel.py +314 -0
- agno/tools/replicate.py +7 -7
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +222 -7
- agno/utils/gemini.py +181 -23
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +95 -5
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/models/cohere.py +1 -1
- agno/utils/models/watsonx.py +1 -1
- agno/utils/openai.py +1 -1
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +183 -135
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +645 -136
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +71 -33
- agno/workflow/workflow.py +2113 -300
- agno-2.3.0.dist-info/METADATA +618 -0
- agno-2.3.0.dist-info/RECORD +577 -0
- agno-2.3.0.dist-info/licenses/LICENSE +201 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.0rc2.dist-info/METADATA +0 -355
- agno-2.0.0rc2.dist-info/RECORD +0 -515
- agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/media.py
CHANGED
|
@@ -1,343 +1,349 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
3
|
+
from uuid import uuid4
|
|
3
4
|
|
|
4
5
|
from pydantic import BaseModel, field_validator, model_validator
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
original_prompt: Optional[str] = None
|
|
10
|
-
revised_prompt: Optional[str] = None
|
|
8
|
+
class Image(BaseModel):
|
|
9
|
+
"""Unified Image class for all use cases (input, output, artifacts)"""
|
|
11
10
|
|
|
11
|
+
# Core content fields (exactly one required)
|
|
12
|
+
url: Optional[str] = None # Remote location
|
|
13
|
+
filepath: Optional[Union[Path, str]] = None # Local file path
|
|
14
|
+
content: Optional[bytes] = None # Raw image bytes (standardized to bytes)
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
mime_type: Optional[str] = None #
|
|
17
|
-
eta: Optional[str] = None
|
|
18
|
-
length: Optional[str] = None
|
|
16
|
+
# Metadata fields
|
|
17
|
+
id: Optional[str] = None # For tracking/referencing
|
|
18
|
+
format: Optional[str] = None # E.g. 'png', 'jpeg', 'webp', 'gif'
|
|
19
|
+
mime_type: Optional[str] = None # E.g. 'image/png', 'image/jpeg'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"content": self.content
|
|
25
|
-
if isinstance(self.content, str)
|
|
26
|
-
else self.content.decode("utf-8")
|
|
27
|
-
if self.content
|
|
28
|
-
else None,
|
|
29
|
-
"mime_type": self.mime_type,
|
|
30
|
-
"eta": self.eta,
|
|
31
|
-
}
|
|
32
|
-
return {k: v for k, v in response_dict.items() if v is not None}
|
|
21
|
+
# Input-specific fields
|
|
22
|
+
detail: Optional[str] = (
|
|
23
|
+
None # low, medium, high or auto (per OpenAI spec https://platform.openai.com/docs/guides/vision?lang=node#low-or-high-fidelity-image-understanding)
|
|
24
|
+
)
|
|
33
25
|
|
|
26
|
+
# Output-specific fields (from tools/LLMs)
|
|
27
|
+
original_prompt: Optional[str] = None # Original generation prompt
|
|
28
|
+
revised_prompt: Optional[str] = None # Revised generation prompt
|
|
29
|
+
alt_text: Optional[str] = None # Alt text description
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
@model_validator(mode="before")
|
|
32
|
+
def validate_and_normalize_content(cls, data: Any):
|
|
33
|
+
"""Ensure exactly one content source and normalize to bytes"""
|
|
34
|
+
if isinstance(data, dict):
|
|
35
|
+
url = data.get("url")
|
|
36
|
+
filepath = data.get("filepath")
|
|
37
|
+
content = data.get("content")
|
|
38
|
+
|
|
39
|
+
# Count non-None sources
|
|
40
|
+
sources = [x for x in [url, filepath, content] if x is not None]
|
|
41
|
+
if len(sources) == 0:
|
|
42
|
+
raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
|
|
43
|
+
elif len(sources) > 1:
|
|
44
|
+
raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
|
|
45
|
+
|
|
46
|
+
# Auto-generate ID if not provided
|
|
47
|
+
if data.get("id") is None:
|
|
48
|
+
data["id"] = str(uuid4())
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
if self.content is None:
|
|
43
|
-
return None
|
|
44
|
-
content_normalised: Union[str, bytes] = self.content
|
|
45
|
-
if content_normalised and isinstance(content_normalised, bytes):
|
|
46
|
-
from base64 import b64encode
|
|
50
|
+
return data
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
except Exception:
|
|
55
|
-
# Last resort: try to convert to base64
|
|
56
|
-
try:
|
|
57
|
-
content_normalised = b64encode(bytes(content_normalised)).decode("utf-8") # type: ignore
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
60
|
-
return content_normalised
|
|
52
|
+
def get_content_bytes(self) -> Optional[bytes]:
|
|
53
|
+
"""Get image content as raw bytes, loading from URL/file if needed"""
|
|
54
|
+
if self.content:
|
|
55
|
+
return self.content
|
|
56
|
+
elif self.url:
|
|
57
|
+
import httpx
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
return httpx.get(self.url).content
|
|
60
|
+
elif self.filepath:
|
|
61
|
+
with open(self.filepath, "rb") as f:
|
|
62
|
+
return f.read()
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def to_base64(self) -> Optional[str]:
|
|
66
|
+
"""Convert content to base64 string for transmission/storage"""
|
|
67
|
+
content_bytes = self.get_content_bytes()
|
|
68
|
+
if content_bytes:
|
|
69
|
+
import base64
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"url": self.url,
|
|
68
|
-
"content": content_normalised,
|
|
69
|
-
"mime_type": self.mime_type,
|
|
70
|
-
"alt_text": self.alt_text,
|
|
71
|
-
}
|
|
72
|
-
return {k: v for k, v in response_dict.items() if v is not None}
|
|
71
|
+
return base64.b64encode(content_bytes).decode("utf-8")
|
|
72
|
+
return None
|
|
73
73
|
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_base64(
|
|
76
|
+
cls,
|
|
77
|
+
base64_content: str,
|
|
78
|
+
id: Optional[str] = None,
|
|
79
|
+
mime_type: Optional[str] = None,
|
|
80
|
+
format: Optional[str] = None,
|
|
81
|
+
**kwargs,
|
|
82
|
+
) -> "Image":
|
|
83
|
+
"""Create Image from base64 content"""
|
|
84
|
+
import base64
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
mime_type: Optional[str] = None
|
|
86
|
+
try:
|
|
87
|
+
content_bytes = base64.b64decode(base64_content)
|
|
88
|
+
except Exception:
|
|
89
|
+
content_bytes = base64_content.encode("utf-8")
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
def validate_exclusive_audio(cls, data: Any):
|
|
83
|
-
"""
|
|
84
|
-
Ensure that either `url` or `base64_audio` is provided, but not both.
|
|
85
|
-
"""
|
|
86
|
-
if data.get("url") and data.get("base64_audio"):
|
|
87
|
-
raise ValueError("Provide either `url` or `base64_audio`, not both.")
|
|
88
|
-
if not data.get("url") and not data.get("base64_audio"):
|
|
89
|
-
raise ValueError("Either `url` or `base64_audio` must be provided.")
|
|
90
|
-
return data
|
|
91
|
+
return cls(content=content_bytes, id=id or str(uuid4()), mime_type=mime_type, format=format, **kwargs)
|
|
91
92
|
|
|
92
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
93
|
-
|
|
93
|
+
def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
|
|
94
|
+
"""Convert to dict, optionally including base64-encoded content"""
|
|
95
|
+
result = {
|
|
94
96
|
"id": self.id,
|
|
95
97
|
"url": self.url,
|
|
96
|
-
"
|
|
98
|
+
"filepath": str(self.filepath) if self.filepath else None,
|
|
99
|
+
"format": self.format,
|
|
97
100
|
"mime_type": self.mime_type,
|
|
98
|
-
"
|
|
101
|
+
"detail": self.detail,
|
|
102
|
+
"original_prompt": self.original_prompt,
|
|
103
|
+
"revised_prompt": self.revised_prompt,
|
|
104
|
+
"alt_text": self.alt_text,
|
|
99
105
|
}
|
|
100
|
-
return {k: v for k, v in response_dict.items() if v is not None}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class Video(BaseModel):
|
|
104
|
-
filepath: Optional[Union[Path, str]] = None # Absolute local location for video
|
|
105
|
-
content: Optional[Any] = None # Actual video bytes content
|
|
106
|
-
url: Optional[str] = None # Remote location for video
|
|
107
|
-
format: Optional[str] = None # E.g. `mp4`, `mov`, `avi`, `mkv`, `webm`, `flv`, `mpeg`, `mpg`, `wmv`, `three_gp`
|
|
108
|
-
|
|
109
|
-
@model_validator(mode="before")
|
|
110
|
-
def validate_data(cls, data: Any):
|
|
111
|
-
"""
|
|
112
|
-
Ensure that exactly one of `filepath`, or `content` or `url` is provided.
|
|
113
|
-
Also converts content to bytes if it's a string.
|
|
114
|
-
"""
|
|
115
|
-
# Extract the values from the input data
|
|
116
|
-
filepath = data.get("filepath")
|
|
117
|
-
content = data.get("content")
|
|
118
|
-
url = data.get("url")
|
|
119
|
-
|
|
120
|
-
# Convert and decompress content to bytes if it's a string
|
|
121
|
-
if content and isinstance(content, str):
|
|
122
|
-
import base64
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
import zlib
|
|
126
|
-
|
|
127
|
-
decoded_content = base64.b64decode(content)
|
|
128
|
-
content = zlib.decompress(decoded_content)
|
|
129
|
-
except Exception:
|
|
130
|
-
content = base64.b64decode(content).decode("utf-8")
|
|
131
|
-
data["content"] = content
|
|
132
|
-
|
|
133
|
-
# Count how many fields are set (not None)
|
|
134
|
-
count = len([field for field in [filepath, content, url] if field is not None])
|
|
135
|
-
|
|
136
|
-
if count == 0:
|
|
137
|
-
raise ValueError("One of `filepath` or `content` or `url` must be provided.")
|
|
138
|
-
elif count > 1:
|
|
139
|
-
raise ValueError("Only one of `filepath` or `content` or `url` should be provided.")
|
|
140
|
-
|
|
141
|
-
return data
|
|
142
|
-
|
|
143
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
144
|
-
import base64
|
|
145
|
-
import zlib
|
|
146
106
|
|
|
147
|
-
|
|
148
|
-
"content"
|
|
149
|
-
zlib.compress(self.content) if isinstance(self.content, bytes) else self.content.encode("utf-8")
|
|
150
|
-
).decode("utf-8")
|
|
151
|
-
if self.content
|
|
152
|
-
else None,
|
|
153
|
-
"filepath": self.filepath,
|
|
154
|
-
"format": self.format,
|
|
155
|
-
}
|
|
156
|
-
return {k: v for k, v in response_dict.items() if v is not None}
|
|
107
|
+
if include_base64_content and self.content:
|
|
108
|
+
result["content"] = self.to_base64()
|
|
157
109
|
|
|
158
|
-
|
|
159
|
-
def from_artifact(cls, artifact: VideoArtifact) -> "Video":
|
|
160
|
-
return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
|
|
110
|
+
return {k: v for k, v in result.items() if v is not None}
|
|
161
111
|
|
|
162
112
|
|
|
163
113
|
class Audio(BaseModel):
|
|
164
|
-
|
|
165
|
-
filepath: Optional[Union[Path, str]] = None # Absolute local location for audio
|
|
166
|
-
url: Optional[str] = None # Remote location for audio
|
|
167
|
-
format: Optional[str] = None
|
|
114
|
+
"""Unified Audio class for all use cases (input, output, artifacts)"""
|
|
168
115
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
Also converts content to bytes if it's a string.
|
|
174
|
-
"""
|
|
175
|
-
# Extract the values from the input data
|
|
176
|
-
filepath = data.get("filepath")
|
|
177
|
-
content = data.get("content")
|
|
178
|
-
url = data.get("url")
|
|
179
|
-
|
|
180
|
-
# Convert and decompress content to bytes if it's a string
|
|
181
|
-
if content and isinstance(content, str):
|
|
182
|
-
import base64
|
|
116
|
+
# Core content fields (exactly one required)
|
|
117
|
+
url: Optional[str] = None
|
|
118
|
+
filepath: Optional[Union[Path, str]] = None
|
|
119
|
+
content: Optional[bytes] = None # Raw audio bytes (standardized to bytes)
|
|
183
120
|
|
|
184
|
-
|
|
185
|
-
|
|
121
|
+
# Metadata fields
|
|
122
|
+
id: Optional[str] = None
|
|
123
|
+
format: Optional[str] = None # E.g. 'mp3', 'wav', 'ogg'
|
|
124
|
+
mime_type: Optional[str] = None # E.g. 'audio/mpeg', 'audio/wav'
|
|
186
125
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
data["content"] = content
|
|
126
|
+
# Audio-specific metadata
|
|
127
|
+
duration: Optional[float] = None # Duration in seconds
|
|
128
|
+
sample_rate: Optional[int] = 24000 # Sample rate in Hz
|
|
129
|
+
channels: Optional[int] = 1 # Number of audio channels
|
|
192
130
|
|
|
193
|
-
|
|
194
|
-
|
|
131
|
+
# Output-specific fields (from LLMs)
|
|
132
|
+
transcript: Optional[str] = None # Text transcript of audio
|
|
133
|
+
expires_at: Optional[int] = None # Expiration timestamp for temporary URLs
|
|
195
134
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
135
|
+
@model_validator(mode="before")
|
|
136
|
+
def validate_and_normalize_content(cls, data: Any):
|
|
137
|
+
"""Ensure exactly one content source and normalize to bytes"""
|
|
138
|
+
if isinstance(data, dict):
|
|
139
|
+
url = data.get("url")
|
|
140
|
+
filepath = data.get("filepath")
|
|
141
|
+
content = data.get("content")
|
|
142
|
+
|
|
143
|
+
sources = [x for x in [url, filepath, content] if x is not None]
|
|
144
|
+
if len(sources) == 0:
|
|
145
|
+
raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
|
|
146
|
+
elif len(sources) > 1:
|
|
147
|
+
raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
|
|
148
|
+
|
|
149
|
+
if data.get("id") is None:
|
|
150
|
+
data["id"] = str(uuid4())
|
|
200
151
|
|
|
201
152
|
return data
|
|
202
153
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
154
|
+
def get_content_bytes(self) -> Optional[bytes]:
|
|
155
|
+
"""Get audio content as raw bytes"""
|
|
156
|
+
if self.content:
|
|
157
|
+
return self.content
|
|
158
|
+
elif self.url:
|
|
159
|
+
import httpx
|
|
206
160
|
|
|
207
|
-
if self.url:
|
|
208
161
|
return httpx.get(self.url).content
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
).decode("utf-8")
|
|
220
|
-
if self.content
|
|
221
|
-
else None,
|
|
222
|
-
"filepath": self.filepath,
|
|
223
|
-
"format": self.format,
|
|
224
|
-
}
|
|
162
|
+
elif self.filepath:
|
|
163
|
+
with open(self.filepath, "rb") as f:
|
|
164
|
+
return f.read()
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def to_base64(self) -> Optional[str]:
|
|
168
|
+
"""Convert content to base64 string"""
|
|
169
|
+
content_bytes = self.get_content_bytes()
|
|
170
|
+
if content_bytes:
|
|
171
|
+
import base64
|
|
225
172
|
|
|
226
|
-
|
|
173
|
+
return base64.b64encode(content_bytes).decode("utf-8")
|
|
174
|
+
return None
|
|
227
175
|
|
|
228
176
|
@classmethod
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
channels: Optional[int] = 1
|
|
242
|
-
|
|
243
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
177
|
+
def from_base64(
|
|
178
|
+
cls,
|
|
179
|
+
base64_content: str,
|
|
180
|
+
id: Optional[str] = None,
|
|
181
|
+
mime_type: Optional[str] = None,
|
|
182
|
+
transcript: Optional[str] = None,
|
|
183
|
+
expires_at: Optional[int] = None,
|
|
184
|
+
sample_rate: Optional[int] = 24000,
|
|
185
|
+
channels: Optional[int] = 1,
|
|
186
|
+
**kwargs,
|
|
187
|
+
) -> "Audio":
|
|
188
|
+
"""Create Audio from base64 content (useful for API responses)"""
|
|
244
189
|
import base64
|
|
245
190
|
|
|
246
|
-
|
|
191
|
+
try:
|
|
192
|
+
content_bytes = base64.b64decode(base64_content)
|
|
193
|
+
except Exception:
|
|
194
|
+
# If not valid base64, encode as UTF-8 bytes
|
|
195
|
+
content_bytes = base64_content.encode("utf-8")
|
|
196
|
+
|
|
197
|
+
return cls(
|
|
198
|
+
content=content_bytes,
|
|
199
|
+
id=id or str(uuid4()),
|
|
200
|
+
mime_type=mime_type,
|
|
201
|
+
transcript=transcript,
|
|
202
|
+
expires_at=expires_at,
|
|
203
|
+
sample_rate=sample_rate,
|
|
204
|
+
channels=channels,
|
|
205
|
+
**kwargs,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
|
|
209
|
+
"""Convert to dict, optionally including base64-encoded content"""
|
|
210
|
+
result = {
|
|
247
211
|
"id": self.id,
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"expires_at": self.expires_at,
|
|
252
|
-
"transcript": self.transcript,
|
|
212
|
+
"url": self.url,
|
|
213
|
+
"filepath": str(self.filepath) if self.filepath else None,
|
|
214
|
+
"format": self.format,
|
|
253
215
|
"mime_type": self.mime_type,
|
|
216
|
+
"duration": self.duration,
|
|
254
217
|
"sample_rate": self.sample_rate,
|
|
255
218
|
"channels": self.channels,
|
|
219
|
+
"transcript": self.transcript,
|
|
220
|
+
"expires_at": self.expires_at,
|
|
256
221
|
}
|
|
257
|
-
return {k: v for k, v in response_dict.items() if v is not None}
|
|
258
222
|
|
|
223
|
+
if include_base64_content and self.content:
|
|
224
|
+
result["content"] = self.to_base64()
|
|
259
225
|
|
|
260
|
-
|
|
261
|
-
url: Optional[str] = None # Remote location for image
|
|
262
|
-
filepath: Optional[Union[Path, str]] = None # Absolute local location for image
|
|
263
|
-
content: Optional[Any] = None # Actual image bytes content
|
|
264
|
-
format: Optional[str] = None # E.g. `png`, `jpeg`, `webp`, `gif`
|
|
265
|
-
detail: Optional[str] = (
|
|
266
|
-
None # low, medium, high or auto (per OpenAI spec https://platform.openai.com/docs/guides/vision?lang=node#low-or-high-fidelity-image-understanding)
|
|
267
|
-
)
|
|
268
|
-
id: Optional[str] = None
|
|
226
|
+
return {k: v for k, v in result.items() if v is not None}
|
|
269
227
|
|
|
270
|
-
@property
|
|
271
|
-
def image_url_content(self) -> Optional[bytes]:
|
|
272
|
-
import httpx
|
|
273
228
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
else:
|
|
277
|
-
return None
|
|
229
|
+
class Video(BaseModel):
|
|
230
|
+
"""Unified Video class for all use cases (input, output, artifacts)"""
|
|
278
231
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
Also converts content to bytes if it's a string.
|
|
284
|
-
"""
|
|
285
|
-
# Extract the values from the input data
|
|
286
|
-
url = data.get("url")
|
|
287
|
-
filepath = data.get("filepath")
|
|
288
|
-
content = data.get("content")
|
|
289
|
-
|
|
290
|
-
# Convert and decompress content to bytes if it's a string
|
|
291
|
-
if content and isinstance(content, str):
|
|
292
|
-
import base64
|
|
232
|
+
# Core content fields (exactly one required)
|
|
233
|
+
url: Optional[str] = None
|
|
234
|
+
filepath: Optional[Union[Path, str]] = None
|
|
235
|
+
content: Optional[bytes] = None # Raw video bytes (standardized to bytes)
|
|
293
236
|
|
|
294
|
-
|
|
295
|
-
|
|
237
|
+
# Metadata fields
|
|
238
|
+
id: Optional[str] = None
|
|
239
|
+
format: Optional[str] = None # E.g. 'mp4', 'mov', 'avi', 'webm'
|
|
240
|
+
mime_type: Optional[str] = None # E.g. 'video/mp4', 'video/quicktime'
|
|
296
241
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
242
|
+
# Video-specific metadata
|
|
243
|
+
duration: Optional[float] = None # Duration in seconds
|
|
244
|
+
width: Optional[int] = None # Video width in pixels
|
|
245
|
+
height: Optional[int] = None # Video height in pixels
|
|
246
|
+
fps: Optional[float] = None # Frames per second
|
|
302
247
|
|
|
303
|
-
|
|
304
|
-
|
|
248
|
+
# Output-specific fields (from tools)
|
|
249
|
+
eta: Optional[str] = None # Estimated time for generation
|
|
250
|
+
original_prompt: Optional[str] = None
|
|
251
|
+
revised_prompt: Optional[str] = None
|
|
305
252
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
253
|
+
@model_validator(mode="before")
|
|
254
|
+
def validate_and_normalize_content(cls, data: Any):
|
|
255
|
+
"""Ensure exactly one content source and normalize to bytes"""
|
|
256
|
+
if isinstance(data, dict):
|
|
257
|
+
url = data.get("url")
|
|
258
|
+
filepath = data.get("filepath")
|
|
259
|
+
content = data.get("content")
|
|
260
|
+
|
|
261
|
+
sources = [x for x in [url, filepath, content] if x is not None]
|
|
262
|
+
if len(sources) == 0:
|
|
263
|
+
raise ValueError("One of 'url', 'filepath', or 'content' must be provided")
|
|
264
|
+
elif len(sources) > 1:
|
|
265
|
+
raise ValueError("Only one of 'url', 'filepath', or 'content' should be provided")
|
|
266
|
+
|
|
267
|
+
if data.get("id") is None:
|
|
268
|
+
data["id"] = str(uuid4())
|
|
310
269
|
|
|
311
270
|
return data
|
|
312
271
|
|
|
313
|
-
def
|
|
272
|
+
def get_content_bytes(self) -> Optional[bytes]:
|
|
273
|
+
"""Get video content as raw bytes"""
|
|
274
|
+
if self.content:
|
|
275
|
+
return self.content
|
|
276
|
+
elif self.url:
|
|
277
|
+
import httpx
|
|
278
|
+
|
|
279
|
+
return httpx.get(self.url).content
|
|
280
|
+
elif self.filepath:
|
|
281
|
+
with open(self.filepath, "rb") as f:
|
|
282
|
+
return f.read()
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def to_base64(self) -> Optional[str]:
|
|
286
|
+
"""Convert content to base64 string"""
|
|
287
|
+
content_bytes = self.get_content_bytes()
|
|
288
|
+
if content_bytes:
|
|
289
|
+
import base64
|
|
290
|
+
|
|
291
|
+
return base64.b64encode(content_bytes).decode("utf-8")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_base64(
|
|
296
|
+
cls,
|
|
297
|
+
base64_content: str,
|
|
298
|
+
id: Optional[str] = None,
|
|
299
|
+
mime_type: Optional[str] = None,
|
|
300
|
+
format: Optional[str] = None,
|
|
301
|
+
**kwargs,
|
|
302
|
+
) -> "Video":
|
|
303
|
+
"""Create Image from base64 content"""
|
|
314
304
|
import base64
|
|
315
|
-
import zlib
|
|
316
305
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
306
|
+
try:
|
|
307
|
+
content_bytes = base64.b64decode(base64_content)
|
|
308
|
+
except Exception:
|
|
309
|
+
content_bytes = base64_content.encode("utf-8")
|
|
310
|
+
|
|
311
|
+
return cls(content=content_bytes, id=id or str(uuid4()), mime_type=mime_type, format=format, **kwargs)
|
|
312
|
+
|
|
313
|
+
def to_dict(self, include_base64_content: bool = True) -> Dict[str, Any]:
|
|
314
|
+
"""Convert to dict, optionally including base64-encoded content"""
|
|
315
|
+
result = {
|
|
316
|
+
"id": self.id,
|
|
324
317
|
"url": self.url,
|
|
325
|
-
"
|
|
318
|
+
"filepath": str(self.filepath) if self.filepath else None,
|
|
319
|
+
"format": self.format,
|
|
320
|
+
"mime_type": self.mime_type,
|
|
321
|
+
"duration": self.duration,
|
|
322
|
+
"width": self.width,
|
|
323
|
+
"height": self.height,
|
|
324
|
+
"fps": self.fps,
|
|
325
|
+
"eta": self.eta,
|
|
326
|
+
"original_prompt": self.original_prompt,
|
|
327
|
+
"revised_prompt": self.revised_prompt,
|
|
326
328
|
}
|
|
327
329
|
|
|
328
|
-
|
|
330
|
+
if include_base64_content and self.content:
|
|
331
|
+
result["content"] = self.to_base64()
|
|
329
332
|
|
|
330
|
-
|
|
331
|
-
def from_artifact(cls, artifact: ImageArtifact) -> "Image":
|
|
332
|
-
return cls(url=artifact.url, content=artifact.content, format=artifact.mime_type)
|
|
333
|
+
return {k: v for k, v in result.items() if v is not None}
|
|
333
334
|
|
|
334
335
|
|
|
335
336
|
class File(BaseModel):
|
|
337
|
+
id: Optional[str] = None
|
|
336
338
|
url: Optional[str] = None
|
|
337
339
|
filepath: Optional[Union[Path, str]] = None
|
|
338
340
|
# Raw bytes content of a file
|
|
339
341
|
content: Optional[Any] = None
|
|
340
342
|
mime_type: Optional[str] = None
|
|
343
|
+
|
|
344
|
+
file_type: Optional[str] = None
|
|
345
|
+
filename: Optional[str] = None
|
|
346
|
+
size: Optional[int] = None
|
|
341
347
|
# External file object (e.g. GeminiFile, must be a valid object as expected by the model you are using)
|
|
342
348
|
external: Optional[Any] = None
|
|
343
349
|
format: Optional[str] = None # E.g. `pdf`, `txt`, `csv`, `xml`, etc.
|
|
@@ -363,7 +369,10 @@ class File(BaseModel):
|
|
|
363
369
|
def valid_mime_types(cls) -> List[str]:
|
|
364
370
|
return [
|
|
365
371
|
"application/pdf",
|
|
372
|
+
"application/json",
|
|
366
373
|
"application/x-javascript",
|
|
374
|
+
"application/json",
|
|
375
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
367
376
|
"text/javascript",
|
|
368
377
|
"application/x-python",
|
|
369
378
|
"text/x-python",
|
|
@@ -376,6 +385,29 @@ class File(BaseModel):
|
|
|
376
385
|
"text/rtf",
|
|
377
386
|
]
|
|
378
387
|
|
|
388
|
+
@classmethod
|
|
389
|
+
def from_base64(
|
|
390
|
+
cls,
|
|
391
|
+
base64_content: str,
|
|
392
|
+
id: Optional[str] = None,
|
|
393
|
+
mime_type: Optional[str] = None,
|
|
394
|
+
filename: Optional[str] = None,
|
|
395
|
+
name: Optional[str] = None,
|
|
396
|
+
format: Optional[str] = None,
|
|
397
|
+
) -> "File":
|
|
398
|
+
"""Create File from base64 encoded content"""
|
|
399
|
+
import base64
|
|
400
|
+
|
|
401
|
+
content_bytes = base64.b64decode(base64_content)
|
|
402
|
+
return cls(
|
|
403
|
+
content=content_bytes,
|
|
404
|
+
id=id,
|
|
405
|
+
mime_type=mime_type,
|
|
406
|
+
filename=filename,
|
|
407
|
+
name=name,
|
|
408
|
+
format=format,
|
|
409
|
+
)
|
|
410
|
+
|
|
379
411
|
@property
|
|
380
412
|
def file_url_content(self) -> Optional[Tuple[bytes, str]]:
|
|
381
413
|
import httpx
|
|
@@ -387,3 +419,44 @@ class File(BaseModel):
|
|
|
387
419
|
return content, mime_type
|
|
388
420
|
else:
|
|
389
421
|
return None
|
|
422
|
+
|
|
423
|
+
def _normalise_content(self) -> Optional[Union[str, bytes]]:
|
|
424
|
+
if self.content is None:
|
|
425
|
+
return None
|
|
426
|
+
content_normalised: Union[str, bytes] = self.content
|
|
427
|
+
if content_normalised and isinstance(content_normalised, bytes):
|
|
428
|
+
from base64 import b64encode
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
if self.mime_type and self.mime_type.startswith("text/"):
|
|
432
|
+
content_normalised = content_normalised.decode("utf-8")
|
|
433
|
+
else:
|
|
434
|
+
content_normalised = b64encode(content_normalised).decode("utf-8")
|
|
435
|
+
except UnicodeDecodeError:
|
|
436
|
+
if isinstance(self.content, bytes):
|
|
437
|
+
content_normalised = b64encode(self.content).decode("utf-8")
|
|
438
|
+
except Exception:
|
|
439
|
+
try:
|
|
440
|
+
if isinstance(self.content, bytes):
|
|
441
|
+
content_normalised = b64encode(self.content).decode("utf-8")
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
return content_normalised
|
|
445
|
+
|
|
446
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
447
|
+
content_normalised = self._normalise_content()
|
|
448
|
+
|
|
449
|
+
response_dict = {
|
|
450
|
+
"id": self.id,
|
|
451
|
+
"url": self.url,
|
|
452
|
+
"filepath": str(self.filepath) if self.filepath else None,
|
|
453
|
+
"content": content_normalised,
|
|
454
|
+
"mime_type": self.mime_type,
|
|
455
|
+
"file_type": self.file_type,
|
|
456
|
+
"filename": self.filename,
|
|
457
|
+
"size": self.size,
|
|
458
|
+
"external": self.external,
|
|
459
|
+
"format": self.format,
|
|
460
|
+
"name": self.name,
|
|
461
|
+
}
|
|
462
|
+
return {k: v for k, v in response_dict.items() if v is not None}
|