diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..96d8453 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Unit Tests + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +jobs: + check-permissions: + runs-on: ubuntu-latest + outputs: + can-run: ${{ steps.check.outputs.can-run }} + steps: + - name: Check if user can run tests + id: check + run: | + # Check if PR author is owner, member, or collaborator + if [[ "${{ github.event.pull_request.author_association }}" == "OWNER" ]] || \ + [[ "${{ github.event.pull_request.author_association }}" == "MEMBER" ]] || \ + [[ "${{ github.event.pull_request.author_association }}" == "COLLABORATOR" ]]; then + echo "can-run=true" >> $GITHUB_OUTPUT + echo "✅ User has permission to run tests" + else + echo "can-run=false" >> $GITHUB_OUTPUT + echo "❌ User does not have permission to run tests" + echo "Only repository owners, members, and collaborators can run tests" + fi + + test: + needs: check-permissions + if: needs.check-permissions.outputs.can-run == 'true' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Poetry + run: pipx install poetry + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'poetry' + + - name: Install dependencies + run: | + poetry env use python${{ matrix.python-version }} + poetry install --with test,lint,typing + + - name: Run unit tests + run: make test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.python-version }} + path: | + .coverage + htmlcov/ + retention-days: 7 + + test-status: + needs: check-permissions + if: needs.check-permissions.outputs.can-run == 'false' + runs-on: ubuntu-latest + steps: + - name: Tests skipped + run: | + echo "::warning::Tests were not run because the PR author (${{ github.event.pull_request.user.login }}) is not a repository owner, member, or collaborator." + echo "Repository owners can manually trigger tests by re-running this workflow." diff --git a/Makefile b/Makefile index 0b279b8..079723f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,16 @@ -.PHONY: all format lint test tests integration_tests docker_tests help extended_tests +.PHONY: all format lint test tests integration_tests docker_tests help extended_tests install # Default target executed when no arguments are given to make. all: help +###################### +# INSTALLATION +###################### + +install: + poetry env use python3.12 + poetry install --with test,lint,typing + # Define a variable for the test file path. TEST_FILE ?= tests/unit_tests/ integration_test integration_tests: TEST_FILE = tests/integration_tests/ @@ -56,6 +64,7 @@ check_imports: $(shell find langchain_parallel -name '*.py') help: @echo '----' + @echo 'install - set up Python 3.12 environment and install dependencies' @echo 'check_imports - check imports' @echo 'format - run code formatters' @echo 'lint - run linters' diff --git a/langchain_parallel/chat_models.py b/langchain_parallel/chat_models.py index a999d5f..eac915e 100644 --- a/langchain_parallel/chat_models.py +++ b/langchain_parallel/chat_models.py @@ -132,13 +132,13 @@ class ChatParallelWeb(BaseChatModel): real-time web research capabilities through an OpenAI-compatible interface. Setup: - Install ``langchain-parallel`` and set environment variable - ``PARALLEL_API_KEY``. + Install `langchain-parallel` and set environment variable + `PARALLEL_API_KEY`. - .. code-block:: bash - - pip install -U langchain-parallel - export PARALLEL_API_KEY="your-api-key" + ```bash + pip install -U langchain-parallel + export PARALLEL_API_KEY="your-api-key" + ``` Key init args — completion params: model: str @@ -160,62 +160,62 @@ class ChatParallelWeb(BaseChatModel): Base URL for Parallel API. Defaults to "https://api.parallel.ai". Instantiate: - .. code-block:: python - - from langchain_parallel import ChatParallelWeb - - llm = ChatParallelWeb( - model="speed", - temperature=0.7, - max_tokens=None, - timeout=None, - max_retries=2, - # api_key="...", - # other params... - ) + ```python + from langchain_parallel import ChatParallelWeb + + llm = ChatParallelWeb( + model="speed", + temperature=0.7, + max_tokens=None, + timeout=None, + max_retries=2, + # api_key="...", + # other params... + ) + ``` Invoke: - .. code-block:: python - - messages = [ - ( - "system", - "You are a helpful assistant with access to real-time web " - "information." - ), - ("human", "What are the latest developments in AI?"), - ] - llm.invoke(messages) + ```python + messages = [ + ( + "system", + "You are a helpful assistant with access to real-time web " + "information." + ), + ("human", "What are the latest developments in AI?"), + ] + llm.invoke(messages) + ``` Stream: - .. code-block:: python - - for chunk in llm.stream(messages): - print(chunk.content, end="") + ```python + for chunk in llm.stream(messages): + print(chunk.content, end="") + ``` Async: - .. code-block:: python + ```python + await llm.ainvoke(messages) - await llm.ainvoke(messages) + # stream: + async for chunk in llm.astream(messages): + print(chunk.content, end="") - # stream: - async for chunk in llm.astream(messages): - print(chunk.content, end="") - - # batch: - await llm.abatch([messages]) + # batch: + await llm.abatch([messages]) + ``` Token usage: - .. code-block:: python - - ai_msg = llm.invoke(messages) - ai_msg.usage_metadata + ```python + ai_msg = llm.invoke(messages) + ai_msg.usage_metadata + ``` Response metadata: - .. code-block:: python - - ai_msg = llm.invoke(messages) - ai_msg.response_metadata + ```python + ai_msg = llm.invoke(messages) + ai_msg.response_metadata + ``` """ diff --git a/langchain_parallel/extract_tool.py b/langchain_parallel/extract_tool.py index 0592639..06d09f4 100644 --- a/langchain_parallel/extract_tool.py +++ b/langchain_parallel/extract_tool.py @@ -75,13 +75,13 @@ class ParallelExtractTool(BaseTool): Parallel Extract API. Setup: - Install ``langchain-parallel`` and set environment variable - ``PARALLEL_API_KEY``. + Install `langchain-parallel` and set environment variable + `PARALLEL_API_KEY`. - .. code-block:: bash - - pip install -U langchain-parallel - export PARALLEL_API_KEY="your-api-key" + ```bash + pip install -U langchain-parallel + export PARALLEL_API_KEY="your-api-key" + ``` Key init args: api_key: Optional[SecretStr] @@ -93,35 +93,35 @@ class ParallelExtractTool(BaseTool): Maximum characters per extracted result. Instantiation: - .. code-block:: python - - from langchain_parallel import ParallelExtractTool + ```python + from langchain_parallel import ParallelExtractTool - # Basic instantiation - tool = ParallelExtractTool() + # Basic instantiation + tool = ParallelExtractTool() - # With custom API key and parameters - tool = ParallelExtractTool( - api_key="your-api-key", - max_chars_per_extract=5000 - ) + # With custom API key and parameters + tool = ParallelExtractTool( + api_key="your-api-key", + max_chars_per_extract=5000 + ) + ``` Invocation: - .. code-block:: python - - # Extract content from URLs - result = tool.invoke({ - "urls": [ - "https://example.com/article1", - "https://example.com/article2" - ] - }) - - # Result is a list of dicts with url, title, and content - for item in result: - print(f"Title: {item['title']}") - print(f"URL: {item['url']}") - print(f"Content: {item['content'][:200]}...") + ```python + # Extract content from URLs + result = tool.invoke({ + "urls": [ + "https://example.com/article1", + "https://example.com/article2" + ] + }) + + # Result is a list of dicts with url, title, and content + for item in result: + print(f"Title: {item['title']}") + print(f"URL: {item['url']}") + print(f"Content: {item['content'][:200]}...") + ``` Response Format: Returns a list of dictionaries, each containing: diff --git a/langchain_parallel/search_tool.py b/langchain_parallel/search_tool.py index ae1d423..1c78d11 100644 --- a/langchain_parallel/search_tool.py +++ b/langchain_parallel/search_tool.py @@ -84,13 +84,13 @@ class ParallelWebSearchTool(BaseTool): and metadata collection. Setup: - Install ``langchain-parallel`` and set environment variable - ``PARALLEL_API_KEY``. + Install `langchain-parallel` and set environment variable + `PARALLEL_API_KEY`. - .. code-block:: bash - - pip install -U langchain-parallel - export PARALLEL_API_KEY="your-api-key" + ```bash + pip install -U langchain-parallel + export PARALLEL_API_KEY="your-api-key" + ``` Key init args: api_key: Optional[SecretStr] @@ -100,118 +100,118 @@ class ParallelWebSearchTool(BaseTool): Base URL for Parallel API. Defaults to "https://api.parallel.ai". Instantiation: - .. code-block:: python - - from langchain_parallel import ParallelWebSearchTool + ```python + from langchain_parallel import ParallelWebSearchTool - # Basic instantiation - tool = ParallelWebSearchTool() + # Basic instantiation + tool = ParallelWebSearchTool() - # With custom API key - tool = ParallelWebSearchTool(api_key="your-api-key") + # With custom API key + tool = ParallelWebSearchTool(api_key="your-api-key") + ``` Basic Usage: - .. code-block:: python - - # Simple objective-based search - result = tool.invoke({ - "objective": "What are the latest developments in AI?" - }) - - # Query-based search with multiple queries - result = tool.invoke({ - "search_queries": [ - "latest AI developments 2024", - "machine learning breakthroughs", - "artificial intelligence news" - ], - "max_results": 10 - }) + ```python + # Simple objective-based search + result = tool.invoke({ + "objective": "What are the latest developments in AI?" + }) + + # Query-based search with multiple queries + result = tool.invoke({ + "search_queries": [ + "latest AI developments 2024", + "machine learning breakthroughs", + "artificial intelligence news" + ], + "max_results": 10 + }) + ``` Domain filtering and advanced options: - .. code-block:: python - - # Domain filtering with fetch policy (using dict format) - result = tool.invoke({ - "objective": "Recent climate change research", - "source_policy": { - "include_domains": ["nature.com", "science.org"], - "exclude_domains": ["reddit.com", "twitter.com"] - }, - "max_results": 15, - "excerpts": {"max_chars_per_result": 2000}, # Auto-converted - "mode": "one-shot", # Use 'agentic' for token-efficient results - "fetch_policy": { # Auto-converted to FetchPolicy - "max_age_seconds": 86400, # 1 day cache - "timeout_seconds": 60 - }, - "include_metadata": True - }) - - # Or use the types directly - from langchain_parallel import ExcerptSettings, FetchPolicy - - result = tool.invoke({ - "objective": "Recent climate change research", - "excerpts": ExcerptSettings(max_chars_per_result=2000), - "fetch_policy": FetchPolicy(max_age_seconds=86400, timeout_seconds=60), - }) + ```python + # Domain filtering with fetch policy (using dict format) + result = tool.invoke({ + "objective": "Recent climate change research", + "source_policy": { + "include_domains": ["nature.com", "science.org"], + "exclude_domains": ["reddit.com", "twitter.com"] + }, + "max_results": 15, + "excerpts": {"max_chars_per_result": 2000}, # Auto-converted + "mode": "one-shot", # Use 'agentic' for token-efficient results + "fetch_policy": { # Auto-converted to FetchPolicy + "max_age_seconds": 86400, # 1 day cache + "timeout_seconds": 60 + }, + "include_metadata": True + }) + + # Or use the types directly + from langchain_parallel import ExcerptSettings, FetchPolicy + + result = tool.invoke({ + "objective": "Recent climate change research", + "excerpts": ExcerptSettings(max_chars_per_result=2000), + "fetch_policy": FetchPolicy(max_age_seconds=86400, timeout_seconds=60), + }) + ``` Async Usage: - .. code-block:: python + ```python + import asyncio - import asyncio - - async def search_async(): - result = await tool.ainvoke({ - "objective": "Latest tech news" - }) - return result + async def search_async(): + result = await tool.ainvoke({ + "objective": "Latest tech news" + }) + return result - result = asyncio.run(search_async()) + result = asyncio.run(search_async()) + ``` Response Format: - .. code-block:: python - - { - "search_id": "search_abc123...", - "results": [ - { - "url": "https://example.com/article", - "title": "Article Title", - "excerpts": [ - "Relevant excerpt from the page...", - "Another important section..." - ] - } - ], - "search_metadata": { - "search_duration_seconds": 2.451, - "search_timestamp": "2024-01-15T10:30:00", - "max_results_requested": 10, - "actual_results_returned": 8, - "search_id": "search_abc123...", - "query_count": 3, - "queries_used": ["query1", "query2", "query3"], - "source_policy_applied": true, - "included_domains": ["nature.com"], - "excluded_domains": ["reddit.com"] + ```python + { + "search_id": "search_abc123...", + "results": [ + { + "url": "https://example.com/article", + "title": "Article Title", + "excerpts": [ + "Relevant excerpt from the page...", + "Another important section..." + ] } + ], + "search_metadata": { + "search_duration_seconds": 2.451, + "search_timestamp": "2024-01-15T10:30:00", + "max_results_requested": 10, + "actual_results_returned": 8, + "search_id": "search_abc123...", + "query_count": 3, + "queries_used": ["query1", "query2", "query3"], + "source_policy_applied": true, + "included_domains": ["nature.com"], + "excluded_domains": ["reddit.com"] } + } + ``` Tool Calling Integration: - .. code-block:: python - - # When used with LangChain agents or chat models with tool calling - from langchain_core.messages import HumanMessage - from langchain_parallel import ChatParallelWeb - - chat = ChatParallelWeb() - chat_with_tools = chat.bind_tools([tool]) - - response = chat_with_tools.invoke([ - HumanMessage(content="Search for the latest AI research papers") - ]) + ```python + # When used with LangChain agents or chat models with tool calling + from langchain_core.messages import HumanMessage + from langchain_parallel import ChatParallelWeb + + chat = ChatParallelWeb() + chat_with_tools = chat.bind_tools([tool]) + + response = chat_with_tools.invoke([ + HumanMessage(content="Search for the latest AI research papers") + ]) + ``` Best Practices: - Use specific objectives for better results