diff --git a/.secrets.baseline b/.secrets.baseline
index 8e14911e5..9bf1ecd58 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -142,7 +142,7 @@
"filename": "deploy/.env.example",
"hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684",
"is_verified": false,
- "line_number": 34
+ "line_number": 35
}
],
"deploy/compose/README.md": [
diff --git a/deploy/.env.example b/deploy/.env.example
index 0f7b367b1..95d33dde3 100644
--- a/deploy/.env.example
+++ b/deploy/.env.example
@@ -18,6 +18,7 @@ NVIDIA_API_KEY=
# Web search (Required)
+YDC_API_KEY=
TAVILY_API_KEY=
# Paper search (Optional — choose one provider)
diff --git a/pyproject.toml b/pyproject.toml
index 864b5efaa..1ccc39953 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -207,6 +207,7 @@ dev = [
"google-scholar-paper-search",
"tavily-web-search",
"exa-web-search",
+ "you-com",
"duckduckgo-news-search",
"polymarket-prediction-market",
"knowledge-layer[all]",
@@ -251,6 +252,7 @@ aiq-agent = { workspace = true }
google-scholar-paper-search = { workspace = true }
tavily-web-search = { workspace = true }
exa-web-search = { workspace = true }
+you-com = { workspace = true }
duckduckgo-news-search = { workspace = true }
polymarket-prediction-market = { workspace = true }
knowledge-layer = { workspace = true }
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 0548232e1..1f9235cd3 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -116,6 +116,7 @@ echo ""
echo "Installing data sources..."
"${UV_BIN}" pip install -e ./sources/tavily_web_search
"${UV_BIN}" pip install -e ./sources/exa_web_search
+"${UV_BIN}" pip install -e ./sources/you_com
"${UV_BIN}" pip install -e ./sources/google_scholar_paper_search
"${UV_BIN}" pip install -e "./sources/knowledge_layer[llamaindex,foundational_rag]"
echo "Data Sources installed"
diff --git a/sources/you_com/README.md b/sources/you_com/README.md
new file mode 100644
index 000000000..635b9b368
--- /dev/null
+++ b/sources/you_com/README.md
@@ -0,0 +1,59 @@
+# You.com Tools
+
+NAT-based tools for the You.com API. Requires a `YDC_API_KEY` environment variable or `api_key` config.
+Create an API key, claim your free credits, and learn more at https://you.com/docs/quickstart.
+
+## Tools
+
+### `you_web_search`
+
+Retrieves relevant search results from the web using You.com [Web Search](https://you.com/docs/api-reference/search/v1-search-post). Supports livecrawl (full page content),
+freshness filtering, safesearch, and news filtering.
+
+The [Web Search](https://you.com/docs/api-reference/search/v1-search-post) endpoint is designed to return LLM-ready web
+results based on a user’s query. Based on a classification mechanism, it can return web results and news associated with
+your query. If you need to feed an LLM with the results of a query that sounds like What are the latest geopolitical
+updates from India, then this endpoint is the right one for you.
+
+Key config:
+- `max_results` — number of results (1–100, default 10)
+- `safesearch` - `off`, `moderate`, `strict`
+- `livecrawl_mode` — `off`, `web`, `news`, `all` (default `web`)
+- `livecrawl_format` — `off`, `markdown`, `html` (default `markdown`)
+- `freshness` — `off`, `day`, `week`, `month`, `year`
+- `max_content_length` — truncate livecrawl content to reduce token usage (default 50000, `None` for unbounded)
+- `include_news_results` — True/False whether or not you want to include results categorized as `news`
+
+### `you_research`
+
+
+[Research](https://you.com/docs/api-reference/research/v1-research) goes beyond a single web search. In response to your
+question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited
+answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust
+and verify.
+
+Key config:
+- `research_effort` — `lite`, `standard` (default), `deep`, `exhaustive`
+
+### `you_finance_research`
+
+The [Finance Research API](https://you.com/docs/api-reference/finance-research/v1-finance_research) is purpose-built
+for financial questions. Like the Research API, it runs multiple searches, reads through sources, and synthesizes
+everything into a thorough, well-cited answer — but its retrieval index is optimized for financial data: earnings
+reports, SEC filings, analyst coverage, market data, and financial news. Use it when you need credible, sourced answers
+to financial questions: company fundamentals, market trends, competitive analysis, earnings summaries, or macroeconomic research.
+
+Key config:
+- `research_effort` — `deep` (default) or `exhaustive` only
+
+### `you_contents`
+
+Extracts clean page content from URLs using the You.com [Contents API](https://you.com/docs/api-reference/contents).
+Pass up to 10 URLs and receive their full content — no HTML parsing required.
+
+Key config:
+- `formats` — list of `markdown`, `html`, `metadata` (default `["markdown", "metadata"]`)
+- `crawl_timeout` — per-URL crawl timeout in seconds (1–60); increase for JavaScript-heavy pages
+
+We want to hear from you. If you hit a configuration issue or have questions, reach out to us at support@you.com.
+For enterprise or private inquiries, reach out to api@you.com.
diff --git a/sources/you_com/pyproject.toml b/sources/you_com/pyproject.toml
new file mode 100644
index 000000000..d11269754
--- /dev/null
+++ b/sources/you_com/pyproject.toml
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = ["setuptools >= 64", "setuptools-scm>=8"]
+
+[tool.setuptools]
+packages = ["you_com"]
+package-dir = {"you_com" = "src"}
+
+[project]
+name = "you-com"
+version = "1.0.0"
+description = "NAT-based You.com tools: web search, contents, research, and finance research"
+readme = "README.md"
+requires-python = ">=3.11,<3.14"
+license = {text = "Apache-2.0"}
+dependencies = [
+ "httpx>=0.24.0",
+ "pydantic>=2.0.0",
+ "langchain-youdotcom>=0.3.1",
+]
+
+[project.entry-points."nat.plugins"]
+you_com = "you_com.register"
diff --git a/sources/you_com/src/__init__.py b/sources/you_com/src/__init__.py
new file mode 100644
index 000000000..22c6f862a
--- /dev/null
+++ b/sources/you_com/src/__init__.py
@@ -0,0 +1,28 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""You.com tools (web search, contents, research, finance research) for NAT."""
+
+from .register import you_contents # noqa: F401
+from .register import you_finance_research # noqa: F401
+from .register import you_research # noqa: F401
+from .register import you_web_search # noqa: F401
+
+__all__ = [
+ "you_finance_research",
+ "you_research",
+ "you_web_search",
+ "you_contents",
+]
diff --git a/sources/you_com/src/register.py b/sources/you_com/src/register.py
new file mode 100644
index 000000000..0d356f49d
--- /dev/null
+++ b/sources/you_com/src/register.py
@@ -0,0 +1,512 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import asyncio
+import enum
+import logging
+import os
+from collections.abc import Callable
+from collections.abc import Coroutine
+from typing import Any
+
+from langchain_youdotcom import YouContentsTool
+from langchain_youdotcom import YouFinanceResearchTool
+from langchain_youdotcom import YouResearchTool
+from langchain_youdotcom import YouSearchTool
+from pydantic import Field
+from pydantic import SecretStr
+from pydantic import field_validator
+
+from nat.builder.builder import Builder
+from nat.builder.function_info import FunctionInfo
+from nat.cli.register_workflow import register_function
+from nat.data_models.function import FunctionBaseConfig
+
+logger = logging.getLogger(__name__)
+
+_missing_key_warned = False
+
+_CACHE_MAX_SIZE = 500
+
+# ---------------------------------------------------------------------------
+# Enums
+# ---------------------------------------------------------------------------
+
+
+class SafesearchMode(enum.Enum):
+ off = "off"
+ moderate = "moderate"
+ strict = "strict"
+
+
+class LivecrawlMode(enum.Enum):
+ off = "off"
+ web = "web"
+ news = "news"
+ all = "all"
+
+
+class LivecrawlFormat(enum.Enum):
+ off = "off"
+ markdown = "markdown"
+ html = "html"
+
+
+class FreshnessMode(enum.Enum):
+ off = "off"
+ day = "day"
+ week = "week"
+ month = "month"
+ year = "year"
+
+
+class ContentsFormat(enum.Enum):
+ markdown = "markdown"
+ html = "html"
+ metadata = "metadata"
+
+
+class ResearchEffort(enum.Enum):
+ lite = "lite"
+ standard = "standard"
+ deep = "deep"
+ exhaustive = "exhaustive"
+
+
+# ---------------------------------------------------------------------------
+# Shared helpers
+# ---------------------------------------------------------------------------
+
+
+def _resolve_api_key(tool_config: "YouToolConfig") -> str | None:
+ return tool_config.api_key.get_secret_value() if tool_config.api_key else os.environ.get("YDC_API_KEY")
+
+
+def _warn_missing_key_once(tool_desc: str) -> None:
+ # Process-once by design: all You.com tools share one key, so one warning is enough.
+ global _missing_key_warned
+ if not _missing_key_warned:
+ logger.warning(
+ "YDC_API_KEY not found. The %s tool will be registered but will "
+ "return an error when called. To enable: set YDC_API_KEY in your environment, "
+ ".env file, or specify api_key in your workflow config.",
+ tool_desc,
+ )
+ _missing_key_warned = True
+
+
+def _stub_message(label: str) -> str:
+ return (
+ f"Error: {label} is unavailable because YDC_API_KEY is not set.\n"
+ "To enable this tool:\n"
+ "1. Get an API key from https://you.com/docs/quickstart\n"
+ "2. Set the API key in your environment or in your .env file\n"
+ "3. Restart the application"
+ )
+
+
+def _make_stub(label: str) -> FunctionInfo:
+ """Return a FunctionInfo that reports the tool is unavailable due to missing key."""
+
+ async def _stub(question: str) -> str:
+ return _stub_message(label)
+
+ _stub.__doc__ = f"{label} (unavailable - missing YDC_API_KEY)."
+ return FunctionInfo.from_fn(_stub, description=_stub.__doc__)
+
+
+async def _run_with_retries(
+ label: str,
+ coro_factory: Callable[[str], Coroutine[Any, Any, str]],
+ question: str,
+ *,
+ max_retries: int,
+ timeout: float | None,
+ cache: dict[str, str],
+) -> str:
+ """Execute coro_factory(question) with caching, timeout, and exponential-backoff retry.
+
+ timeout is per-attempt, not end-to-end. Worst-case wall time: timeout * max_retries + backoff.
+ """
+ if question in cache:
+ logger.debug("Cache hit for query: %s", question[:80])
+ return cache[question]
+
+ for attempt in range(max_retries):
+ try:
+ coro = coro_factory(question)
+ result = await (asyncio.wait_for(coro, timeout=timeout) if timeout else coro)
+ if not result or not str(result).strip():
+ raise ValueError(f"{label} returned no results.")
+
+ if len(cache) >= _CACHE_MAX_SIZE:
+ cache.pop(next(iter(cache)))
+ cache[question] = result
+ return result
+
+ except Exception as e:
+ if attempt == max_retries - 1:
+ error_msg = str(e)
+ if isinstance(e, ValueError):
+ return f"Error: {error_msg}"
+ if "401" in error_msg or "Unauthorized" in error_msg:
+ return (
+ f"Error: {label} failed due to invalid API key (401 Unauthorized).\n"
+ "Please check your YDC_API_KEY and ensure it is valid.\n"
+ )
+ return f"Error: {label} failed after {max_retries} attempts: {error_msg}"
+ await asyncio.sleep(2**attempt)
+
+
+# ---------------------------------------------------------------------------
+# Shared base config
+# ---------------------------------------------------------------------------
+
+
+class YouToolConfig(FunctionBaseConfig):
+ """Base config shared by all You.com tools. Not registered directly."""
+
+ api_key: SecretStr | None = Field(default=None, description="The API key for the You.com service")
+ max_retries: int = Field(default=3, ge=1, description="Maximum number of retries for the request")
+ timeout: float | None = Field(
+ default=None,
+ description="Timeout in seconds per attempt. None means no timeout.",
+ )
+
+
+# ---------------------------------------------------------------------------
+# Tool configs
+# ---------------------------------------------------------------------------
+
+
+class YouWebSearchToolConfig(YouToolConfig, name="you_web_search"):
+ """
+ Tool that retrieves relevant search results from web search (using You.com) for the given question.
+ Uses LangChain's YouSearchAPIWrapper. Requires a YDC_API_KEY environment variable or api_key config.
+ """
+
+ max_results: int = Field(default=10, ge=1, le=100, description="Maximum number of search results to return")
+ safesearch: SafesearchMode = Field(
+ default=SafesearchMode.moderate, description="Safesearch filter: 'off', 'moderate', or 'strict'"
+ )
+ livecrawl_mode: LivecrawlMode = Field(
+ default=LivecrawlMode.web,
+ description="If you want to retrieve page contents and the format to retrieve in: "
+ "'off', 'web', 'news', or 'all'",
+ )
+ livecrawl_format: LivecrawlFormat = Field(
+ default=LivecrawlFormat.markdown,
+ description="What format you want to retrieve content in: 'off', 'markdown', 'html'",
+ )
+ freshness: FreshnessMode = Field(
+ default=FreshnessMode.off,
+ description="Restrict your search to a certain freshness: 'off', 'day', 'week', 'month', 'year'",
+ )
+ max_content_length: int | None = Field(
+ default=50000,
+ ge=0,
+ description="If set, truncates each livecrawl result to specified amount of characters. "
+ "Can be used to reduce token usage. Defaults to 50000 to bound context size; "
+ "set to None for unbounded livecrawl content. "
+ "Titles and descriptions always remain fully in tact, only livecrawl content is truncated.",
+ )
+ include_news_results: bool = Field(
+ default=False,
+ description="Whether or not you want to include news results. If False, filter out documents whose "
+ "metadata 'source' is 'news'.",
+ )
+
+
+_FINANCE_RESEARCH_EFFORTS = {ResearchEffort.deep, ResearchEffort.exhaustive}
+
+
+class YouFinanceResearchToolConfig(YouToolConfig, name="you_finance_research"):
+ """
+ Tool that answers financial questions using the You.com Finance Research API.
+ Searches a finance-optimized index (SEC filings, earnings, equity prices, macro
+ indicators) and returns a cited markdown response. Requires YDC_API_KEY.
+ """
+
+ research_effort: ResearchEffort = Field(
+ default=ResearchEffort.deep,
+ description="Research depth: 'deep' (faster) or 'exhaustive' (up to 300s)",
+ )
+
+ @field_validator("research_effort")
+ @classmethod
+ def _finance_effort_must_be_deep_or_exhaustive(cls, v: ResearchEffort) -> ResearchEffort:
+ if v not in _FINANCE_RESEARCH_EFFORTS:
+ raise ValueError(f"Finance research only supports 'deep' or 'exhaustive', got '{v.value}'")
+ return v
+
+
+class YouContentsToolConfig(YouToolConfig, name="you_contents"):
+ """
+ Tool that extracts clean content from URLs using the You.com Contents API.
+ Pass up to 10 URLs and receive their full page content as Markdown, HTML, or metadata.
+ Requires YDC_API_KEY.
+ """
+
+ formats: list[ContentsFormat] = Field(
+ default=[ContentsFormat.markdown, ContentsFormat.metadata],
+ description="Content formats to return: 'markdown', 'html', 'metadata'",
+ )
+ crawl_timeout: float | None = Field(
+ default=None,
+ ge=1,
+ le=60,
+ description="Per-URL crawl timeout in seconds (1-60). Increase for JavaScript-heavy pages.",
+ )
+
+
+class YouResearchToolConfig(YouToolConfig, name="you_research"):
+ """
+ Tool that answers open-domain questions using the You.com Research API.
+ Synthesizes a cited markdown answer from live web sources. Requires YDC_API_KEY.
+ """
+
+ research_effort: ResearchEffort = Field(
+ default=ResearchEffort.standard,
+ description="Research depth: 'lite', 'standard', 'deep', or 'exhaustive' (slower, more thorough)",
+ )
+
+
+# ---------------------------------------------------------------------------
+# Tool registrations
+# ---------------------------------------------------------------------------
+
+
+@register_function(config_type=YouWebSearchToolConfig)
+async def you_web_search(tool_config: YouWebSearchToolConfig, builder: Builder):
+ api_key = _resolve_api_key(tool_config)
+
+ if not api_key:
+ _warn_missing_key_once("web search")
+ yield _make_stub("Web search")
+ return
+
+ livecrawl_mode = (
+ None if tool_config.livecrawl_mode.value == LivecrawlMode.off.value else tool_config.livecrawl_mode.value
+ )
+ livecrawl_format = (
+ None if tool_config.livecrawl_format.value == LivecrawlFormat.off.value else tool_config.livecrawl_format.value
+ )
+ freshness = None if tool_config.freshness == FreshnessMode.off else tool_config.freshness.value
+ wrapper_kwargs = {
+ k: v
+ for k, v in {
+ "ydc_api_key": api_key,
+ "count": tool_config.max_results,
+ "livecrawl": livecrawl_mode,
+ "livecrawl_formats": livecrawl_format,
+ "freshness": freshness,
+ "safesearch": tool_config.safesearch.value,
+ }.items()
+ if v is not None
+ }
+ you_search_tool = YouSearchTool(api_wrapper=wrapper_kwargs)
+
+ _cache: dict[str, str] = {}
+
+ def _format_documents(search_docs) -> list[str]:
+ formatted_results = []
+ for doc in search_docs:
+ if not tool_config.include_news_results and doc.metadata.get("source") == "news":
+ continue
+ title = doc.metadata.get("title", "")
+ url = doc.metadata.get("url", "")
+ description = doc.metadata.get("description", "")
+ content = doc.page_content
+ if content:
+ if tool_config.max_content_length:
+ content = content[: tool_config.max_content_length]
+ formatted_results.append(
+ f'
content
") + ] + await info.single_fn(["https://example.com"]) + + _, call_kwargs = mock_contents["tool"].api_wrapper.contents_async.call_args + assert call_kwargs.get("formats") == ["html"] + + async def test_cache_returns_same_result(self, mock_contents, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouContentsToolConfig() + builder = MagicMock() + + async with you_contents(config, builder) as info: + mock_contents["tool"].api_wrapper.contents_async.return_value = [ + _make_doc("https://example.com", "Example", "cached content") + ] + out1 = await info.single_fn(["https://example.com"]) + out2 = await info.single_fn(["https://example.com"]) + + assert out1 == out2 + assert mock_contents["tool"].api_wrapper.contents_async.call_count == 1 diff --git a/sources/you_com/tests/test_you_finance_research.py b/sources/you_com/tests/test_you_finance_research.py new file mode 100644 index 000000000..6679c4874 --- /dev/null +++ b/sources/you_com/tests/test_you_finance_research.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the you_finance_research NAT tool registration.""" + +import asyncio +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from pydantic import SecretStr +from you_com.register import ResearchEffort +from you_com.register import YouFinanceResearchToolConfig +from you_com.register import you_finance_research + + +@pytest.fixture +def mock_finance(monkeypatch): + captured = {} + + def factory(**kwargs): + wrapper = MagicMock() + wrapper.finance_text_async = AsyncMock(return_value="## Answer\n\nSome financial insight.\n") + tool = MagicMock(api_wrapper=wrapper) + captured["tool"] = tool + captured["kwargs"] = kwargs + return tool + + monkeypatch.setattr("you_com.register.YouFinanceResearchTool", factory) + return captured + + +class TestYouFinanceResearchToolConfig: + def test_defaults(self): + config = YouFinanceResearchToolConfig() + assert config.research_effort == ResearchEffort.deep + assert config.api_key is None + assert config.max_retries == 3 + assert config.timeout is None + + def test_inherits_from_function_base_config(self): + from nat.data_models.function import FunctionBaseConfig + + assert issubclass(YouFinanceResearchToolConfig, FunctionBaseConfig) + + def test_rejects_lite_effort(self): + with pytest.raises(Exception, match="deep.*exhaustive|exhaustive.*deep"): + YouFinanceResearchToolConfig(research_effort=ResearchEffort.lite) + + def test_rejects_standard_effort(self): + with pytest.raises(Exception, match="deep.*exhaustive|exhaustive.*deep"): + YouFinanceResearchToolConfig(research_effort=ResearchEffort.standard) + + +class TestYouFinanceResearchStub: + async def test_stub_when_no_api_key(self): + config = YouFinanceResearchToolConfig() + builder = MagicMock() + + async with you_finance_research(config, builder) as info: + result = await info.single_fn("anything") + + assert "YDC_API_KEY" in result + assert "unavailable" in result.lower() + + +class TestYouFinanceResearchLive: + async def test_config_api_key_used(self, mock_finance, monkeypatch): + config = YouFinanceResearchToolConfig(api_key=SecretStr("key-from-config")) + builder = MagicMock() + + async with you_finance_research(config, builder) as _: + pass + + assert mock_finance["kwargs"].get("api_wrapper", {}).get("ydc_api_key") == "key-from-config" + + async def test_successful_call_returns_markdown(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouFinanceResearchToolConfig() + builder = MagicMock() + + async with you_finance_research(config, builder) as info: + out = await info.single_fn("What drove NVIDIA revenue growth?") + + assert "Answer" in out + assert "financial insight" in out + + async def test_empty_result_returns_error(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouFinanceResearchToolConfig(max_retries=1) + builder = MagicMock() + + async with you_finance_research(config, builder) as info: + mock_finance["tool"].api_wrapper.finance_text_async.return_value = "" + out = await info.single_fn("q") + + assert "no results" in out.lower() + + async def test_retries_then_succeeds(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouFinanceResearchToolConfig(max_retries=3) + builder = MagicMock() + + async with you_finance_research(config, builder) as info: + mock_finance["tool"].api_wrapper.finance_text_async.side_effect = [ + RuntimeError("transient"), + "## Answer\n\nRecovered.\n", + ] + out = await info.single_fn("q") + + assert "Recovered" in out + assert mock_finance["tool"].api_wrapper.finance_text_async.call_count == 2 + + async def test_401_returns_friendly_message(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouFinanceResearchToolConfig(max_retries=2) + builder = MagicMock() + + async with you_finance_research(config, builder) as info: + mock_finance["tool"].api_wrapper.finance_text_async.side_effect = RuntimeError("401 Unauthorized") + out = await info.single_fn("q") + + assert "401" in out + + async def test_timeout_applied(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouFinanceResearchToolConfig(max_retries=1, timeout=0.001) + builder = MagicMock() + + async def _hang(_): + await asyncio.sleep(10) + + async with you_finance_research(config, builder) as info: + mock_finance["tool"].api_wrapper.finance_text_async.side_effect = _hang + out = await info.single_fn("q") + + assert "error" in out.lower() + + async def test_research_effort_passed_to_tool(self, mock_finance, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouFinanceResearchToolConfig(research_effort=ResearchEffort.exhaustive) + builder = MagicMock() + + async with you_finance_research(config, builder) as _: + pass + + assert mock_finance["kwargs"].get("api_wrapper", {}).get("research_effort") == "exhaustive" diff --git a/sources/you_com/tests/test_you_helpers.py b/sources/you_com/tests/test_you_helpers.py new file mode 100644 index 000000000..75acb733c --- /dev/null +++ b/sources/you_com/tests/test_you_helpers.py @@ -0,0 +1,208 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for shared helpers and tool registration smoke tests.""" + +import asyncio +from unittest.mock import AsyncMock + +import pytest +from pydantic import SecretStr +from you_com.register import YouContentsToolConfig +from you_com.register import YouFinanceResearchToolConfig +from you_com.register import YouResearchToolConfig +from you_com.register import YouToolConfig +from you_com.register import YouWebSearchToolConfig +from you_com.register import _make_stub +from you_com.register import _resolve_api_key +from you_com.register import _run_with_retries +from you_com.register import _warn_missing_key_once + + +@pytest.fixture(autouse=True) +def _reset_warn_flag(): + import you_com.register as reg + + reg._missing_key_warned = False + yield + reg._missing_key_warned = False + + +@pytest.fixture(autouse=True) +def _clear_env(monkeypatch): + monkeypatch.delenv("YDC_API_KEY", raising=False) + + +# --------------------------------------------------------------------------- +# Registration smoke tests — guards that all three tools have the right name +# --------------------------------------------------------------------------- + + +class TestRegistrationNames: + def test_web_search_name(self): + assert YouWebSearchToolConfig._typed_model_name == "you_web_search" + + def test_finance_research_name(self): + assert YouFinanceResearchToolConfig._typed_model_name == "you_finance_research" + + def test_research_name(self): + assert YouResearchToolConfig._typed_model_name == "you_research" + + def test_contents_name(self): + assert YouContentsToolConfig._typed_model_name == "you_contents" + + def test_base_config_not_registered(self): + assert YouToolConfig._typed_model_name is None + + +# --------------------------------------------------------------------------- +# _resolve_api_key +# --------------------------------------------------------------------------- + + +class TestResolveApiKey: + def test_returns_env_key_when_no_config_key(self, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "from-env") + config = YouToolConfig() + assert _resolve_api_key(config) == "from-env" + + def test_returns_config_key(self, monkeypatch): + config = YouToolConfig(api_key=SecretStr("from-config")) + assert _resolve_api_key(config) == "from-config" + + def test_config_key_takes_precedence_over_env(self, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "from-env") + config = YouToolConfig(api_key=SecretStr("from-config")) + assert _resolve_api_key(config) == "from-config" + + def test_returns_none_when_no_key(self): + config = YouToolConfig() + assert _resolve_api_key(config) is None + + +# --------------------------------------------------------------------------- +# _warn_missing_key_once +# --------------------------------------------------------------------------- + + +class TestWarnMissingKeyOnce: + def test_warns_only_once(self, caplog): + import logging + + with caplog.at_level(logging.WARNING, logger="you_com.register"): + _warn_missing_key_once("web search") + _warn_missing_key_once("web search") + + assert sum(1 for r in caplog.records if "YDC_API_KEY" in r.message) == 1 + + +# --------------------------------------------------------------------------- +# _make_stub +# --------------------------------------------------------------------------- + + +class TestMakeStub: + async def test_stub_contains_label_and_unavailable(self): + stub = _make_stub("Finance research") + result = await stub.single_fn("anything") + assert "Finance research" in result + assert "unavailable" in result.lower() + assert "YDC_API_KEY" in result + + async def test_stub_different_labels_produce_different_messages(self): + web = _make_stub("Web search") + finance = _make_stub("Finance research") + web_result = await web.single_fn("q") + finance_result = await finance.single_fn("q") + assert "Web search" in web_result + assert "Finance research" in finance_result + assert web_result != finance_result + + +# --------------------------------------------------------------------------- +# _run_with_retries +# --------------------------------------------------------------------------- + + +class TestRunWithRetries: + async def test_success_on_first_attempt(self): + factory = AsyncMock(return_value="ok result") + cache: dict = {} + out = await _run_with_retries("T", factory, "q", max_retries=3, timeout=None, cache=cache) + assert out == "ok result" + assert cache["q"] == "ok result" + + async def test_cache_hit_skips_factory(self): + factory = AsyncMock(return_value="fresh") + cache = {"q": "cached"} + out = await _run_with_retries("T", factory, "q", max_retries=3, timeout=None, cache=cache) + assert out == "cached" + factory.assert_not_called() + + async def test_retries_on_transient_error(self, monkeypatch): + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + factory = AsyncMock(side_effect=[RuntimeError("transient"), "recovered"]) + cache: dict = {} + out = await _run_with_retries("T", factory, "q", max_retries=3, timeout=None, cache=cache) + assert out == "recovered" + assert factory.call_count == 2 + + async def test_exhausted_retries_returns_error_string(self, monkeypatch): + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + factory = AsyncMock(side_effect=RuntimeError("boom")) + cache: dict = {} + out = await _run_with_retries("T", factory, "q", max_retries=2, timeout=None, cache=cache) + assert "error" in out.lower() + assert "boom" in out + + async def test_empty_result_returns_no_results_message(self, monkeypatch): + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + factory = AsyncMock(return_value="") + cache: dict = {} + out = await _run_with_retries("T", factory, "q", max_retries=1, timeout=None, cache=cache) + assert "no results" in out.lower() + assert out.startswith("Error: ") + + async def test_401_returns_friendly_message(self, monkeypatch): + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + factory = AsyncMock(side_effect=RuntimeError("401 Unauthorized")) + cache: dict = {} + out = await _run_with_retries("T", factory, "q", max_retries=1, timeout=None, cache=cache) + assert "401" in out + + async def test_timeout_applied(self): + async def _hang(_q): + await asyncio.sleep(10) + + cache: dict = {} + out = await _run_with_retries("T", _hang, "q", max_retries=1, timeout=0.001, cache=cache) + assert "error" in out.lower() + + async def test_cache_evicts_oldest_when_full(self, monkeypatch): + from you_com import register as reg + + original = reg._CACHE_MAX_SIZE + monkeypatch.setattr(reg, "_CACHE_MAX_SIZE", 2) + try: + factory = AsyncMock(side_effect=["r1", "r2", "r3"]) + cache: dict = {} + await _run_with_retries("T", factory, "q1", max_retries=1, timeout=None, cache=cache) + await _run_with_retries("T", factory, "q2", max_retries=1, timeout=None, cache=cache) + await _run_with_retries("T", factory, "q3", max_retries=1, timeout=None, cache=cache) + assert "q1" not in cache + assert "q2" in cache + assert "q3" in cache + finally: + monkeypatch.setattr(reg, "_CACHE_MAX_SIZE", original) diff --git a/sources/you_com/tests/test_you_research.py b/sources/you_com/tests/test_you_research.py new file mode 100644 index 000000000..82ac27c7b --- /dev/null +++ b/sources/you_com/tests/test_you_research.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the you_research NAT tool registration.""" + +import asyncio +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from pydantic import SecretStr +from you_com.register import ResearchEffort +from you_com.register import YouResearchToolConfig +from you_com.register import you_research + + +@pytest.fixture +def mock_research(monkeypatch): + captured = {} + + def factory(**kwargs): + wrapper = MagicMock() + wrapper.research_text_async = AsyncMock( + return_value="## Answer\n\nSome research findings.\n\n[1] [Source Title](https://example.com)\n" + ) + tool = MagicMock(api_wrapper=wrapper) + captured["tool"] = tool + captured["kwargs"] = kwargs + return tool + + monkeypatch.setattr("you_com.register.YouResearchTool", factory) + return captured + + +class TestYouResearchToolConfig: + def test_defaults(self): + config = YouResearchToolConfig() + assert config.research_effort == ResearchEffort.standard + assert config.api_key is None + assert config.max_retries == 3 + assert config.timeout is None + + def test_inherits_from_function_base_config(self): + from nat.data_models.function import FunctionBaseConfig + + assert issubclass(YouResearchToolConfig, FunctionBaseConfig) + + +class TestYouResearchStub: + async def test_stub_when_no_api_key(self): + config = YouResearchToolConfig() + builder = MagicMock() + + async with you_research(config, builder) as info: + result = await info.single_fn("anything") + + assert "YDC_API_KEY" in result + assert "unavailable" in result.lower() + + +class TestYouResearchLive: + async def test_config_api_key_used(self, mock_research, monkeypatch): + config = YouResearchToolConfig(api_key=SecretStr("key-from-config")) + builder = MagicMock() + + async with you_research(config, builder) as _: + pass + + assert mock_research["kwargs"].get("api_wrapper", {}).get("ydc_api_key") == "key-from-config" + + async def test_successful_call_returns_markdown(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouResearchToolConfig() + builder = MagicMock() + + async with you_research(config, builder) as info: + out = await info.single_fn("What is quantum computing?") + + assert "Answer" in out + assert "research findings" in out + + async def test_empty_result_returns_error(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouResearchToolConfig(max_retries=1) + builder = MagicMock() + + async with you_research(config, builder) as info: + mock_research["tool"].api_wrapper.research_text_async.return_value = "" + out = await info.single_fn("q") + + assert "no results" in out.lower() + + async def test_retries_then_succeeds(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouResearchToolConfig(max_retries=3) + builder = MagicMock() + + async with you_research(config, builder) as info: + mock_research["tool"].api_wrapper.research_text_async.side_effect = [ + RuntimeError("transient"), + "## Answer\n\nRecovered.\n", + ] + out = await info.single_fn("q") + + assert "Recovered" in out + assert mock_research["tool"].api_wrapper.research_text_async.call_count == 2 + + async def test_401_returns_friendly_message(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouResearchToolConfig(max_retries=2) + builder = MagicMock() + + async with you_research(config, builder) as info: + mock_research["tool"].api_wrapper.research_text_async.side_effect = RuntimeError("401 Unauthorized") + out = await info.single_fn("q") + + assert "401" in out + + async def test_timeout_applied(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouResearchToolConfig(max_retries=1, timeout=0.001) + builder = MagicMock() + + async def _hang(_): + await asyncio.sleep(10) + + async with you_research(config, builder) as info: + mock_research["tool"].api_wrapper.research_text_async.side_effect = _hang + out = await info.single_fn("q") + + assert "error" in out.lower() + + async def test_research_effort_passed_to_tool(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouResearchToolConfig(research_effort=ResearchEffort.deep) + builder = MagicMock() + + async with you_research(config, builder) as _: + pass + + assert mock_research["kwargs"].get("api_wrapper", {}).get("research_effort") == "deep" + + async def test_cache_returns_same_result(self, mock_research, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouResearchToolConfig() + builder = MagicMock() + + async with you_research(config, builder) as info: + out1 = await info.single_fn("cached question") + out2 = await info.single_fn("cached question") + + assert out1 == out2 + assert mock_research["tool"].api_wrapper.research_text_async.call_count == 1 diff --git a/sources/you_com/tests/test_you_web_search.py b/sources/you_com/tests/test_you_web_search.py new file mode 100644 index 000000000..c6ba4406a --- /dev/null +++ b/sources/you_com/tests/test_you_web_search.py @@ -0,0 +1,231 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the you_web_search NAT tool registration.""" + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from pydantic import SecretStr +from you_com.register import FreshnessMode +from you_com.register import YouWebSearchToolConfig +from you_com.register import you_web_search + + +def _make_doc(title="Title", url="https://example.com", description="", page_content="content", source=None): + metadata = {"title": title, "url": url, "description": description} + if source: + metadata["source"] = source + return SimpleNamespace(metadata=metadata, page_content=page_content) + + +@pytest.fixture +def mock_search(monkeypatch): + captured = {} + + def factory(api_wrapper): + wrapper = MagicMock() + wrapper.results_async = AsyncMock(return_value=[]) + tool = MagicMock(api_wrapper=wrapper) + captured["tool"] = tool + captured["kwargs"] = api_wrapper + return tool + + monkeypatch.setattr("you_com.register.YouSearchTool", factory) + return captured + + +class TestYouWebSearchToolConfig: + def test_defaults(self): + config = YouWebSearchToolConfig() + assert config.max_results == 10 + assert config.api_key is None + assert config.max_retries == 3 + assert config.safesearch.value == "moderate" + assert config.livecrawl_mode.value == "web" + assert config.livecrawl_format.value == "markdown" + assert config.freshness == FreshnessMode.off + assert config.max_content_length == 50000 + assert config.include_news_results is False + assert config.timeout is None + + def test_inherits_from_function_base_config(self): + from nat.data_models.function import FunctionBaseConfig + + assert issubclass(YouWebSearchToolConfig, FunctionBaseConfig) + + +class TestYouWebSearchStub: + async def test_stub_when_no_api_key(self): + config = YouWebSearchToolConfig() + builder = MagicMock() + + async with you_web_search(config, builder) as info: + result = await info.single_fn("anything") + + assert "YDC_API_KEY" in result + assert "unavailable" in result.lower() + + +class TestYouWebSearchLive: + async def test_config_api_key_used(self, mock_search, monkeypatch): + config = YouWebSearchToolConfig(api_key=SecretStr("key-from-config")) + builder = MagicMock() + + async with you_web_search(config, builder) as _: + pass + + assert mock_search["kwargs"].get("ydc_api_key") == "key-from-config" + + async def test_successful_search_formats_documents(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(max_results=2) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [ + _make_doc("Title A", "https://a.example", page_content="Body A"), + _make_doc("Title B", "https://b.example", page_content="Body B"), + ] + out = await info.single_fn("query") + + assert "Title A" in out + assert "Title B" in out + assert "Body A" in out + assert "Body B" in out + assert "---" in out + + async def test_max_content_length_truncates(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(max_content_length=5) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [ + _make_doc(page_content="abcdefghijklmnop"), + ] + out = await info.single_fn("q") + + assert "abcde" in out + assert "abcdefgh" not in out + + async def test_default_bounds_oversized_livecrawl_content(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(max_results=10) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [ + _make_doc(page_content="x" * 200_000) for _ in range(10) + ] + out = await info.single_fn("q") + + assert len(out) <= 10 * 50000 + 10_000 + + async def test_news_source_filtered_by_default(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(include_news_results=False) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [ + _make_doc("Web", "https://web.example", page_content="web body"), + _make_doc("News", "https://news.example", page_content="news body", source="news"), + ] + out = await info.single_fn("q") + + assert "https://web.example" in out + assert "https://news.example" not in out + + async def test_include_news_results_keeps_news(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(include_news_results=True) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [ + _make_doc("Web", "https://web.example", page_content="web body"), + _make_doc("News", "https://news.example", page_content="news body", source="news"), + ] + out = await info.single_fn("q") + + assert "https://web.example" in out + assert "https://news.example" in out + + async def test_empty_results_returns_error(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(max_retries=1) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.return_value = [] + out = await info.single_fn("q") + + assert "no results" in out.lower() + + async def test_retries_then_succeeds(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouWebSearchToolConfig(max_retries=3) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.side_effect = [ + RuntimeError("transient"), + [_make_doc("T", "https://a.example", page_content="ok")], + ] + out = await info.single_fn("q") + + assert "ok" in out + assert mock_search["tool"].api_wrapper.results_async.call_count == 2 + + async def test_401_returns_friendly_message(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + monkeypatch.setattr("you_com.register.asyncio.sleep", AsyncMock()) + config = YouWebSearchToolConfig(max_retries=2) + builder = MagicMock() + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.side_effect = RuntimeError("401 Unauthorized") + out = await info.single_fn("q") + + assert "401" in out + + async def test_wrapper_kwargs_exclude_none_freshness(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig() # freshness=off → mapped to None before request + builder = MagicMock() + + async with you_web_search(config, builder) as _: + pass + + assert "freshness" not in mock_search["kwargs"] + + async def test_timeout_applied(self, mock_search, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "test-key") + config = YouWebSearchToolConfig(max_retries=1, timeout=0.001) + builder = MagicMock() + + async def _hang(_): + await asyncio.sleep(10) + + async with you_web_search(config, builder) as info: + mock_search["tool"].api_wrapper.results_async.side_effect = _hang + out = await info.single_fn("q") + + assert "error" in out.lower() diff --git a/uv.lock b/uv.lock index ee6196c52..e5e151a29 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,7 @@ members = [ "knowledge-layer", "polymarket-prediction-market", "tavily-web-search", + "you-com", ] overrides = [ { name = "authlib", specifier = ">=1.6.11,<2" }, @@ -266,6 +267,7 @@ dev = [ { name = "ruff" }, { name = "tavily-web-search" }, { name = "yapf" }, + { name = "you-com" }, ] [package.metadata] @@ -330,6 +332,7 @@ dev = [ { name = "ruff", specifier = "~=0.15.1" }, { name = "tavily-web-search", editable = "sources/tavily_web_search" }, { name = "yapf", specifier = ">=0.40.0" }, + { name = "you-com", editable = "sources/you_com" }, ] [[package]] @@ -2931,6 +2934,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, ] +[[package]] +name = "langchain-youdotcom" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "youdotcom" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/9e/4d6fe20f509f04b245502b0816b2282232b4ef4ade14d98ec2d79b1869a4/langchain_youdotcom-0.3.1.tar.gz", hash = "sha256:dd0ad3358ad3590c0fffe673d0de173ce6d0e7930792893f0c5a5019b0b042b4", size = 104026, upload-time = "2026-06-12T22:04:28.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/e7/5e772c2ca51cf4fbe9d3dc606faae8f2024b3c08243f2d664b445ae6aff6/langchain_youdotcom-0.3.1-py3-none-any.whl", hash = "sha256:b76a01bdb2e7668ef3649370a625eeedd65a34a8fd68fbb997c74fd0d232b518", size = 12515, upload-time = "2026-06-12T22:04:29.299Z" }, +] + [[package]] name = "langgraph" version = "1.2.8" @@ -7278,6 +7294,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] +[[package]] +name = "you-com" +version = "1.0.0" +source = { editable = "sources/you_com" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-youdotcom" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.24.0" }, + { name = "langchain-youdotcom", specifier = ">=0.3.1" }, + { name = "pydantic", specifier = ">=2.0.0" }, +] + +[[package]] +name = "youdotcom" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpcore" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/6b/1d3dd77f5968086d1606972697074ddd07b72abcb514c38a39feda77031f/youdotcom-2.3.0.tar.gz", hash = "sha256:f35e4ba53bb7f235100e7b7fcccfdcb84e9a08c85bb053d4e6c76bf29ecc7547", size = 71806, upload-time = "2026-03-04T03:21:11.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/0c/a57beaa961914c9014ee7ffc16697844874e4a7910848f11a55b45b58619/youdotcom-2.3.0-py3-none-any.whl", hash = "sha256:5750d0bcc88cf666b88e36e116c9775607db5d3491a02afad219eb9d4ec97eb0", size = 89388, upload-time = "2026-03-04T03:21:09.308Z" }, +] + [[package]] name = "zict" version = "3.0.0"