From e56690abef787792d715a4c42e0f68d230578f07 Mon Sep 17 00:00:00 2001 From: Ben Browning Date: Fri, 20 Jun 2025 18:09:14 -0400 Subject: [PATCH 1/3] feat: Add synthetic-data-kit for file_search doc conversion This adds a `builtin::document_conversion` tool for converting documents when used with file_search that uses meta-llama/synthetic-data-kit. I also have another local implementation that uses Docling, but need to debug some segfault issues I'm hitting locally with that so pushing this first as a simpler reference implementation. Long-term I think we'll want a remote implemention here as well - like perhaps docling-serve or unstructured.io - but need to look more into that. This passes the existing `tests/verifications/openai_api/test_responses.py` but doesn't yet add any new tests for file types besides text and pdf. Signed-off-by: Ben Browning --- .../self_hosted_distro/ollama.md | 2 +- .../synthetic-data-kit/__init__.py | 19 +++ .../tool_runtime/synthetic-data-kit/config.py | 15 +++ .../synthetic-data-kit/synthetic_data_kit.py | 117 ++++++++++++++++++ .../inline/vector_io/faiss/__init__.py | 4 +- .../providers/inline/vector_io/faiss/faiss.py | 10 +- .../inline/vector_io/sqlite_vec/__init__.py | 4 +- .../inline/vector_io/sqlite_vec/sqlite_vec.py | 6 +- .../providers/registry/tool_runtime.py | 8 ++ llama_stack/providers/registry/vector_io.py | 8 +- .../utils/memory/openai_vector_store_mixin.py | 28 +++-- llama_stack/templates/ollama/build.yaml | 1 + llama_stack/templates/ollama/ollama.py | 5 + .../templates/ollama/run-with-safety.yaml | 5 + llama_stack/templates/ollama/run.yaml | 5 + llama_stack/templates/starter/build.yaml | 1 + llama_stack/templates/starter/run.yaml | 5 + llama_stack/templates/starter/starter.py | 5 + 18 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 llama_stack/providers/inline/tool_runtime/synthetic-data-kit/__init__.py create mode 100644 llama_stack/providers/inline/tool_runtime/synthetic-data-kit/config.py create mode 100644 llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py diff --git a/docs/source/distributions/self_hosted_distro/ollama.md b/docs/source/distributions/self_hosted_distro/ollama.md index e09c79359..4a77d5bd9 100644 --- a/docs/source/distributions/self_hosted_distro/ollama.md +++ b/docs/source/distributions/self_hosted_distro/ollama.md @@ -24,7 +24,7 @@ The `llamastack/distribution-ollama` distribution consists of the following prov | safety | `inline::llama-guard` | | scoring | `inline::basic`, `inline::llm-as-judge`, `inline::braintrust` | | telemetry | `inline::meta-reference` | -| tool_runtime | `remote::brave-search`, `remote::tavily-search`, `inline::rag-runtime`, `remote::model-context-protocol`, `remote::wolfram-alpha` | +| tool_runtime | `remote::brave-search`, `remote::tavily-search`, `inline::rag-runtime`, `inline::synthetic-data-kit`, `remote::model-context-protocol`, `remote::wolfram-alpha` | | vector_io | `inline::faiss`, `remote::chromadb`, `remote::pgvector` | diff --git a/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/__init__.py b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/__init__.py new file mode 100644 index 000000000..fd722c0ec --- /dev/null +++ b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +from typing import Any + +from llama_stack.providers.datatypes import Api + +from .config import SyntheticDataKitToolRuntimeConfig + + +async def get_provider_impl(config: SyntheticDataKitToolRuntimeConfig, deps: dict[Api, Any]): + from .synthetic_data_kit import SyntheticDataKitToolRuntimeImpl + + impl = SyntheticDataKitToolRuntimeImpl(config, deps[Api.files]) + await impl.initialize() + return impl diff --git a/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/config.py b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/config.py new file mode 100644 index 000000000..eae7c7550 --- /dev/null +++ b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/config.py @@ -0,0 +1,15 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +from typing import Any + +from pydantic import BaseModel + + +class SyntheticDataKitToolRuntimeConfig(BaseModel): + @classmethod + def sample_run_config(cls, __distro_dir__: str, **kwargs: Any) -> dict[str, Any]: + return {} diff --git a/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py new file mode 100644 index 000000000..450ba65b1 --- /dev/null +++ b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py @@ -0,0 +1,117 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + + +import asyncio +import logging +import mimetypes +import os +import tempfile +from typing import Any + +from llama_stack.apis.common.content_types import URL +from llama_stack.apis.files.files import Files +from llama_stack.apis.tools import ( + ListToolDefsResponse, + ToolDef, + ToolGroup, + ToolInvocationResult, + ToolParameter, + ToolRuntime, +) +from llama_stack.providers.datatypes import ToolGroupsProtocolPrivate +from llama_stack.providers.utils.memory.vector_store import content_from_data_and_mime_type + +from .config import SyntheticDataKitToolRuntimeConfig + +log = logging.getLogger(__name__) + + +class SyntheticDataKitToolRuntimeImpl(ToolGroupsProtocolPrivate, ToolRuntime): + def __init__( + self, + config: SyntheticDataKitToolRuntimeConfig, + files_api: Files, + ): + self.config = config + self.files_api = files_api + + async def initialize(self): + pass + + async def shutdown(self): + pass + + async def register_toolgroup(self, toolgroup: ToolGroup) -> None: + pass + + async def unregister_toolgroup(self, toolgroup_id: str) -> None: + return + + async def list_runtime_tools( + self, tool_group_id: str | None = None, mcp_endpoint: URL | None = None + ) -> ListToolDefsResponse: + return ListToolDefsResponse( + data=[ + ToolDef( + name="convert_file_to_text", + description="Convert a file to text", + parameters=[ + ToolParameter( + name="file_id", + description="The id of the file to convert.", + parameter_type="string", + ), + ], + ), + ] + ) + + async def invoke_tool(self, tool_name: str, kwargs: dict[str, Any]) -> ToolInvocationResult: + if tool_name != "convert_file_to_text": + raise ValueError(f"Unknown tool: {tool_name}") + + file_id = kwargs["file_id"] + file_response = await self.files_api.openai_retrieve_file(file_id) + mime_type, _ = mimetypes.guess_type(file_response.filename) + content_response = await self.files_api.openai_retrieve_file_content(file_id) + + mime_category = mime_type.split("/")[0] if mime_type else None + if mime_category == "text": + # Don't use synthetic-data-kit if the file is already text + content = content_from_data_and_mime_type(content_response.body, mime_type) + return ToolInvocationResult( + content=content, + metadata={}, + ) + else: + return await asyncio.to_thread( + self.synthetic_data_kit_convert, content_response.body, file_response.filename + ) + + def synthetic_data_kit_convert(self, content_body: bytes, filename: str) -> ToolInvocationResult: + from synthetic_data_kit.core.ingest import process_file + + try: + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, filename) + with open(file_path, "wb") as f: + f.write(content_body) + output_path = process_file(file_path, tmpdir) + with open(output_path) as f: + content = f.read() + + return ToolInvocationResult( + content=content, + metadata={}, + ) + except Exception as e: + return ToolInvocationResult( + content="", + error_message=f"Error converting file: {e}", + error_code=1, + metadata={}, + ) diff --git a/llama_stack/providers/inline/vector_io/faiss/__init__.py b/llama_stack/providers/inline/vector_io/faiss/__init__.py index dd1c59b7b..d97cd91b2 100644 --- a/llama_stack/providers/inline/vector_io/faiss/__init__.py +++ b/llama_stack/providers/inline/vector_io/faiss/__init__.py @@ -16,6 +16,8 @@ async def get_provider_impl(config: FaissVectorIOConfig, deps: dict[Api, Any]): assert isinstance(config, FaissVectorIOConfig), f"Unexpected config type: {type(config)}" - impl = FaissVectorIOAdapter(config, deps[Api.inference], deps.get(Api.files, None)) + impl = FaissVectorIOAdapter( + config, deps[Api.inference], deps.get(Api.files, None), deps.get(Api.tool_runtime, None) + ) await impl.initialize() return impl diff --git a/llama_stack/providers/inline/vector_io/faiss/faiss.py b/llama_stack/providers/inline/vector_io/faiss/faiss.py index 12f4d6ad0..c83c9e46d 100644 --- a/llama_stack/providers/inline/vector_io/faiss/faiss.py +++ b/llama_stack/providers/inline/vector_io/faiss/faiss.py @@ -18,6 +18,7 @@ from numpy.typing import NDArray from llama_stack.apis.files import Files from llama_stack.apis.inference import InterleavedContent from llama_stack.apis.inference.inference import Inference +from llama_stack.apis.tools.tools import ToolRuntime from llama_stack.apis.vector_dbs import VectorDB from llama_stack.apis.vector_io import ( Chunk, @@ -150,10 +151,17 @@ class FaissIndex(EmbeddingIndex): class FaissVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO, VectorDBsProtocolPrivate): - def __init__(self, config: FaissVectorIOConfig, inference_api: Inference, files_api: Files | None) -> None: + def __init__( + self, + config: FaissVectorIOConfig, + inference_api: Inference, + files_api: Files | None = None, + tool_runtime_api: ToolRuntime | None = None, + ) -> None: self.config = config self.inference_api = inference_api self.files_api = files_api + self.tool_runtime_api = tool_runtime_api self.cache: dict[str, VectorDBWithIndex] = {} self.kvstore: KVStore | None = None self.openai_vector_stores: dict[str, dict[str, Any]] = {} diff --git a/llama_stack/providers/inline/vector_io/sqlite_vec/__init__.py b/llama_stack/providers/inline/vector_io/sqlite_vec/__init__.py index e5200a755..3786bb12b 100644 --- a/llama_stack/providers/inline/vector_io/sqlite_vec/__init__.py +++ b/llama_stack/providers/inline/vector_io/sqlite_vec/__init__.py @@ -15,6 +15,8 @@ async def get_provider_impl(config: SQLiteVectorIOConfig, deps: dict[Api, Any]): from .sqlite_vec import SQLiteVecVectorIOAdapter assert isinstance(config, SQLiteVectorIOConfig), f"Unexpected config type: {type(config)}" - impl = SQLiteVecVectorIOAdapter(config, deps[Api.inference], deps.get(Api.files, None)) + impl = SQLiteVecVectorIOAdapter( + config, deps[Api.inference], deps.get(Api.files, None), deps.get(Api.tool_runtime, None) + ) await impl.initialize() return impl diff --git a/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py b/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py index d832e56f5..b73297da0 100644 --- a/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py +++ b/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py @@ -19,6 +19,7 @@ from numpy.typing import NDArray from llama_stack.apis.files.files import Files from llama_stack.apis.inference.inference import Inference +from llama_stack.apis.tools.tools import ToolRuntime from llama_stack.apis.vector_dbs import VectorDB from llama_stack.apis.vector_io import ( Chunk, @@ -434,10 +435,13 @@ class SQLiteVecVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO, VectorDBsProtoc and creates a cache of VectorDBWithIndex instances (each wrapping a SQLiteVecIndex). """ - def __init__(self, config, inference_api: Inference, files_api: Files | None) -> None: + def __init__( + self, config, inference_api: Inference, files_api: Files | None, tool_runtime_api: ToolRuntime | None + ) -> None: self.config = config self.inference_api = inference_api self.files_api = files_api + self.tool_runtime_api = tool_runtime_api self.cache: dict[str, VectorDBWithIndex] = {} self.openai_vector_stores: dict[str, dict[str, Any]] = {} diff --git a/llama_stack/providers/registry/tool_runtime.py b/llama_stack/providers/registry/tool_runtime.py index fa359f6b5..9362a7297 100644 --- a/llama_stack/providers/registry/tool_runtime.py +++ b/llama_stack/providers/registry/tool_runtime.py @@ -34,6 +34,14 @@ def available_providers() -> list[ProviderSpec]: config_class="llama_stack.providers.inline.tool_runtime.rag.config.RagToolRuntimeConfig", api_dependencies=[Api.vector_io, Api.inference], ), + InlineProviderSpec( + api=Api.tool_runtime, + provider_type="inline::synthetic-data-kit", + pip_packages=["synthetic-data-kit"], + module="llama_stack.providers.inline.tool_runtime.synthetic-data-kit", + config_class="llama_stack.providers.inline.tool_runtime.synthetic-data-kit.config.SyntheticDataKitToolRuntimeConfig", + api_dependencies=[Api.files], + ), remote_provider_spec( api=Api.tool_runtime, adapter=AdapterSpec( diff --git a/llama_stack/providers/registry/vector_io.py b/llama_stack/providers/registry/vector_io.py index 55c1b5617..d367d4982 100644 --- a/llama_stack/providers/registry/vector_io.py +++ b/llama_stack/providers/registry/vector_io.py @@ -24,7 +24,7 @@ def available_providers() -> list[ProviderSpec]: config_class="llama_stack.providers.inline.vector_io.faiss.FaissVectorIOConfig", deprecation_warning="Please use the `inline::faiss` provider instead.", api_dependencies=[Api.inference], - optional_api_dependencies=[Api.files], + optional_api_dependencies=[Api.files, Api.tool_runtime], ), InlineProviderSpec( api=Api.vector_io, @@ -33,7 +33,7 @@ def available_providers() -> list[ProviderSpec]: module="llama_stack.providers.inline.vector_io.faiss", config_class="llama_stack.providers.inline.vector_io.faiss.FaissVectorIOConfig", api_dependencies=[Api.inference], - optional_api_dependencies=[Api.files], + optional_api_dependencies=[Api.files, Api.tool_runtime], ), # NOTE: sqlite-vec cannot be bundled into the container image because it does not have a # source distribution and the wheels are not available for all platforms. @@ -44,7 +44,7 @@ def available_providers() -> list[ProviderSpec]: module="llama_stack.providers.inline.vector_io.sqlite_vec", config_class="llama_stack.providers.inline.vector_io.sqlite_vec.SQLiteVectorIOConfig", api_dependencies=[Api.inference], - optional_api_dependencies=[Api.files], + optional_api_dependencies=[Api.files, Api.tool_runtime], ), InlineProviderSpec( api=Api.vector_io, @@ -54,7 +54,7 @@ def available_providers() -> list[ProviderSpec]: config_class="llama_stack.providers.inline.vector_io.sqlite_vec.SQLiteVectorIOConfig", deprecation_warning="Please use the `inline::sqlite-vec` provider (notice the hyphen instead of underscore) instead.", api_dependencies=[Api.inference], - optional_api_dependencies=[Api.files], + optional_api_dependencies=[Api.files, Api.tool_runtime], ), remote_provider_spec( Api.vector_io, diff --git a/llama_stack/providers/utils/memory/openai_vector_store_mixin.py b/llama_stack/providers/utils/memory/openai_vector_store_mixin.py index 9c0e1dbe7..2099e3b1e 100644 --- a/llama_stack/providers/utils/memory/openai_vector_store_mixin.py +++ b/llama_stack/providers/utils/memory/openai_vector_store_mixin.py @@ -6,14 +6,14 @@ import asyncio import logging -import mimetypes import time import uuid from abc import ABC, abstractmethod -from typing import Any +from typing import Any, cast from llama_stack.apis.files import Files from llama_stack.apis.files.files import OpenAIFileObject +from llama_stack.apis.tools.tools import ToolRuntime from llama_stack.apis.vector_dbs import VectorDB from llama_stack.apis.vector_io import ( QueryChunksResponse, @@ -38,7 +38,7 @@ from llama_stack.apis.vector_io.vector_io import ( VectorStoreFileStatus, VectorStoreListFilesResponse, ) -from llama_stack.providers.utils.memory.vector_store import content_from_data_and_mime_type, make_overlapped_chunks +from llama_stack.providers.utils.memory.vector_store import make_overlapped_chunks logger = logging.getLogger(__name__) @@ -56,6 +56,7 @@ class OpenAIVectorStoreMixin(ABC): # These should be provided by the implementing class openai_vector_stores: dict[str, dict[str, Any]] files_api: Files | None + tool_runtime_api: ToolRuntime | None @abstractmethod async def _save_openai_vector_store(self, store_id: str, store_info: dict[str, Any]) -> None: @@ -525,6 +526,14 @@ class OpenAIVectorStoreMixin(ABC): ) return vector_store_file_object + if not hasattr(self, "tool_runtime_api") or not self.tool_runtime_api: + vector_store_file_object.status = "failed" + vector_store_file_object.last_error = VectorStoreFileLastError( + code="server_error", + message="Tool runtime API is not available", + ) + return vector_store_file_object + if isinstance(chunking_strategy, VectorStoreChunkingStrategyStatic): max_chunk_size_tokens = chunking_strategy.static.max_chunk_size_tokens chunk_overlap_tokens = chunking_strategy.static.chunk_overlap_tokens @@ -534,12 +543,13 @@ class OpenAIVectorStoreMixin(ABC): chunk_overlap_tokens = 400 try: - file_response = await self.files_api.openai_retrieve_file(file_id) - mime_type, _ = mimetypes.guess_type(file_response.filename) - content_response = await self.files_api.openai_retrieve_file_content(file_id) - - content = content_from_data_and_mime_type(content_response.body, mime_type) - + tool_result = await self.tool_runtime_api.invoke_tool( + "convert_file_to_text", + {"file_id": file_id}, + ) + if tool_result.error_code or tool_result.error_message: + raise ValueError(f"Failed to convert file to text: {tool_result.error_message}") + content = cast(str, tool_result.content) # The tool always returns strings chunks = make_overlapped_chunks( file_id, content, diff --git a/llama_stack/templates/ollama/build.yaml b/llama_stack/templates/ollama/build.yaml index ebe0849f3..a75785b47 100644 --- a/llama_stack/templates/ollama/build.yaml +++ b/llama_stack/templates/ollama/build.yaml @@ -31,6 +31,7 @@ distribution_spec: - remote::brave-search - remote::tavily-search - inline::rag-runtime + - inline::synthetic-data-kit - remote::model-context-protocol - remote::wolfram-alpha image_type: conda diff --git a/llama_stack/templates/ollama/ollama.py b/llama_stack/templates/ollama/ollama.py index 46c4852a4..6c5a737bf 100644 --- a/llama_stack/templates/ollama/ollama.py +++ b/llama_stack/templates/ollama/ollama.py @@ -36,6 +36,7 @@ def get_distribution_template() -> DistributionTemplate: "remote::brave-search", "remote::tavily-search", "inline::rag-runtime", + "inline::synthetic-data-kit", "remote::model-context-protocol", "remote::wolfram-alpha", ], @@ -91,6 +92,10 @@ def get_distribution_template() -> DistributionTemplate: toolgroup_id="builtin::wolfram_alpha", provider_id="wolfram-alpha", ), + ToolGroupInput( + toolgroup_id="builtin::document_conversion", + provider_id="synthetic-data-kit", + ), ] return DistributionTemplate( diff --git a/llama_stack/templates/ollama/run-with-safety.yaml b/llama_stack/templates/ollama/run-with-safety.yaml index 85d5c813b..3dbda7773 100644 --- a/llama_stack/templates/ollama/run-with-safety.yaml +++ b/llama_stack/templates/ollama/run-with-safety.yaml @@ -114,6 +114,9 @@ providers: - provider_id: rag-runtime provider_type: inline::rag-runtime config: {} + - provider_id: synthetic-data-kit + provider_type: inline::synthetic-data-kit + config: {} - provider_id: model-context-protocol provider_type: remote::model-context-protocol config: {} @@ -158,5 +161,7 @@ tool_groups: provider_id: rag-runtime - toolgroup_id: builtin::wolfram_alpha provider_id: wolfram-alpha +- toolgroup_id: builtin::document_conversion + provider_id: synthetic-data-kit server: port: 8321 diff --git a/llama_stack/templates/ollama/run.yaml b/llama_stack/templates/ollama/run.yaml index 2d10a99a4..d6d88feda 100644 --- a/llama_stack/templates/ollama/run.yaml +++ b/llama_stack/templates/ollama/run.yaml @@ -112,6 +112,9 @@ providers: - provider_id: rag-runtime provider_type: inline::rag-runtime config: {} + - provider_id: synthetic-data-kit + provider_type: inline::synthetic-data-kit + config: {} - provider_id: model-context-protocol provider_type: remote::model-context-protocol config: {} @@ -148,5 +151,7 @@ tool_groups: provider_id: rag-runtime - toolgroup_id: builtin::wolfram_alpha provider_id: wolfram-alpha +- toolgroup_id: builtin::document_conversion + provider_id: synthetic-data-kit server: port: 8321 diff --git a/llama_stack/templates/starter/build.yaml b/llama_stack/templates/starter/build.yaml index 9bf4913a7..418b22ceb 100644 --- a/llama_stack/templates/starter/build.yaml +++ b/llama_stack/templates/starter/build.yaml @@ -38,6 +38,7 @@ distribution_spec: - remote::brave-search - remote::tavily-search - inline::rag-runtime + - inline::synthetic-data-kit - remote::model-context-protocol image_type: conda additional_pip_packages: diff --git a/llama_stack/templates/starter/run.yaml b/llama_stack/templates/starter/run.yaml index 960e96d01..a7498b090 100644 --- a/llama_stack/templates/starter/run.yaml +++ b/llama_stack/templates/starter/run.yaml @@ -155,6 +155,9 @@ providers: - provider_id: rag-runtime provider_type: inline::rag-runtime config: {} + - provider_id: synthetic-data-kit + provider_type: inline::synthetic-data-kit + config: {} - provider_id: model-context-protocol provider_type: remote::model-context-protocol config: {} @@ -954,5 +957,7 @@ tool_groups: provider_id: tavily-search - toolgroup_id: builtin::rag provider_id: rag-runtime +- toolgroup_id: builtin::document_conversion + provider_id: synthetic-data-kit server: port: 8321 diff --git a/llama_stack/templates/starter/starter.py b/llama_stack/templates/starter/starter.py index 2a44a0a37..c6bcc3019 100644 --- a/llama_stack/templates/starter/starter.py +++ b/llama_stack/templates/starter/starter.py @@ -146,6 +146,7 @@ def get_distribution_template() -> DistributionTemplate: "remote::brave-search", "remote::tavily-search", "inline::rag-runtime", + "inline::synthetic-data-kit", "remote::model-context-protocol", ], } @@ -192,6 +193,10 @@ def get_distribution_template() -> DistributionTemplate: toolgroup_id="builtin::rag", provider_id="rag-runtime", ), + ToolGroupInput( + toolgroup_id="builtin::document_conversion", + provider_id="synthetic-data-kit", + ), ] embedding_model = ModelInput( model_id="all-MiniLM-L6-v2", From dae7953de460d20f5ba0b840ba9ce0e9ce21e843 Mon Sep 17 00:00:00 2001 From: Ben Browning Date: Fri, 20 Jun 2025 19:02:07 -0400 Subject: [PATCH 2/3] Still retrieve the file_response in openai_vector_store_mixin This is needed to get the filename of our file, even though we don't need its actual contents here anymore. Signed-off-by: Ben Browning --- llama_stack/providers/utils/memory/openai_vector_store_mixin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/llama_stack/providers/utils/memory/openai_vector_store_mixin.py b/llama_stack/providers/utils/memory/openai_vector_store_mixin.py index 2099e3b1e..5f4777adf 100644 --- a/llama_stack/providers/utils/memory/openai_vector_store_mixin.py +++ b/llama_stack/providers/utils/memory/openai_vector_store_mixin.py @@ -543,6 +543,7 @@ class OpenAIVectorStoreMixin(ABC): chunk_overlap_tokens = 400 try: + file_response = await self.files_api.openai_retrieve_file(file_id) tool_result = await self.tool_runtime_api.invoke_tool( "convert_file_to_text", {"file_id": file_id}, From fb6763eef5903ee6644d4507b27c375cc9cd6377 Mon Sep 17 00:00:00 2001 From: Ben Browning Date: Sat, 21 Jun 2025 09:31:38 -0400 Subject: [PATCH 3/3] Expand file types tested with file_search This expands the file types tested with file_search to include Word documents (.docx), Markdown (.md), text (.txt), PDF (.pdf), and PowerPoint (.pptx) files. Python's mimetypes library doesn't actually recognize markdown docs as text, so we have to handle that case specifically instead of relying on mimetypes to get it right. Signed-off-by: Ben Browning --- .../synthetic-data-kit/synthetic_data_kit.py | 12 +++++-- .../fixtures/docs/llama_stack_and_models.docx | Bin 0 -> 7769 bytes .../fixtures/docs/llama_stack_and_models.md | 27 ++++++++++++++++ .../{pdfs => docs}/llama_stack_and_models.pdf | Bin .../fixtures/docs/llama_stack_and_models.pptx | Bin 0 -> 41466 bytes .../fixtures/docs/llama_stack_and_models.txt | 24 ++++++++++++++ .../fixtures/test_cases/responses.yaml | 30 +++++++++++++++++- 7 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.docx create mode 100644 tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.md rename tests/verifications/openai_api/fixtures/{pdfs => docs}/llama_stack_and_models.pdf (100%) create mode 100644 tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.pptx create mode 100644 tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.txt diff --git a/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py index 450ba65b1..e06b5ee97 100644 --- a/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py +++ b/llama_stack/providers/inline/tool_runtime/synthetic-data-kit/synthetic_data_kit.py @@ -76,7 +76,7 @@ class SyntheticDataKitToolRuntimeImpl(ToolGroupsProtocolPrivate, ToolRuntime): file_id = kwargs["file_id"] file_response = await self.files_api.openai_retrieve_file(file_id) - mime_type, _ = mimetypes.guess_type(file_response.filename) + mime_type = self._guess_mime_type(file_response.filename) content_response = await self.files_api.openai_retrieve_file_content(file_id) mime_category = mime_type.split("/")[0] if mime_type else None @@ -89,10 +89,16 @@ class SyntheticDataKitToolRuntimeImpl(ToolGroupsProtocolPrivate, ToolRuntime): ) else: return await asyncio.to_thread( - self.synthetic_data_kit_convert, content_response.body, file_response.filename + self._synthetic_data_kit_convert, content_response.body, file_response.filename ) - def synthetic_data_kit_convert(self, content_body: bytes, filename: str) -> ToolInvocationResult: + def _guess_mime_type(self, filename: str) -> str | None: + mime_type, _ = mimetypes.guess_type(filename) + if mime_type is None and filename.endswith(".md"): + mime_type = "text/markdown" + return mime_type + + def _synthetic_data_kit_convert(self, content_body: bytes, filename: str) -> ToolInvocationResult: from synthetic_data_kit.core.ingest import process_file try: diff --git a/tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.docx b/tests/verifications/openai_api/fixtures/docs/llama_stack_and_models.docx new file mode 100644 index 0000000000000000000000000000000000000000..b9d6223fe658294784defdaa79c3295397dcafe9 GIT binary patch literal 7769 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 sWxoF