diff --git a/kfinance/CHANGELOG.md b/kfinance/CHANGELOG.md index 8b0f41c..763e1bf 100644 --- a/kfinance/CHANGELOG.md +++ b/kfinance/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v5.1.4 +- Update tools to gracefully handle more errors + ## v5.1.3 - Bump fastmcp and langchain dependencies diff --git a/kfinance/client/id_resolution.py b/kfinance/client/id_resolution.py index e346a5e..711e9c7 100644 --- a/kfinance/client/id_resolution.py +++ b/kfinance/client/id_resolution.py @@ -9,5 +9,6 @@ async def unified_fetch_id_triples( """Resolve one or more identifiers to id triples using the unified (/ids) endpoint.""" resp = await httpx_client.post(url="/ids", json=dict(identifiers=identifiers)) + resp.raise_for_status() resp_json = resp.json() return UnifiedIdTripleResponse.model_validate(resp_json) diff --git a/kfinance/domains/companies/company_tools.py b/kfinance/domains/companies/company_tools.py index a970214..f5c7c95 100644 --- a/kfinance/domains/companies/company_tools.py +++ b/kfinance/domains/companies/company_tools.py @@ -216,6 +216,7 @@ async def fetch_info_from_company_id( """Fetch and return company info for one company_id.""" url = f"/info/{company_id}" resp = await httpx_client.get(url=url) + resp.raise_for_status() return resp.json() @@ -316,7 +317,10 @@ async def get_company_summary_or_description_from_identifiers( result = task.result.summary else: result = task.result.description - results[task.result_key] = result + if not result: + errors.append(f"No {summary_or_description} found for {task.result_key}.") + else: + results[task.result_key] = result if summary_or_description == "summary": return GetCompanySummaryFromIdentifiersResp(results=results, errors=errors) diff --git a/kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py b/kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py index 3259c6b..183e6d7 100644 --- a/kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +++ b/kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py @@ -126,4 +126,5 @@ async def fetch_cusip_or_isin_from_security_id( """Fetch and return the cusip or isin for a security id.""" url = f"/{cusip_or_isin}/{security_id}" resp = await httpx_client.get(url=url) + resp.raise_for_status() return resp.json()[cusip_or_isin] diff --git a/kfinance/domains/earnings/earning_tools.py b/kfinance/domains/earnings/earning_tools.py index cd5e237..9a65fe7 100644 --- a/kfinance/domains/earnings/earning_tools.py +++ b/kfinance/domains/earnings/earning_tools.py @@ -246,6 +246,7 @@ async def get_transcript_from_key_dev_id( """Fetch raw transcript text for a key_dev_id.""" url = f"/transcript/{key_dev_id}" resp = await httpx_client.get(url=url) + resp.raise_for_status() transcript_data = resp.json() # Convert transcript components to raw text format (same as sync version) diff --git a/kfinance/domains/estimates/estimates_tools.py b/kfinance/domains/estimates/estimates_tools.py index 156bd4e..8ed2635 100644 --- a/kfinance/domains/estimates/estimates_tools.py +++ b/kfinance/domains/estimates/estimates_tools.py @@ -275,6 +275,7 @@ async def fetch_estimates_from_company_id( params["num_periods_backward"] = num_periods_backward resp = await httpx_client.post(url="/estimates/", json=params) + resp.raise_for_status() return SingleResultResp[Estimates].model_validate(resp.json()) @@ -324,6 +325,7 @@ async def fetch_consensus_target_price_from_company_id( ) -> SingleResultResp[ConsensusTargetPrice]: """Fetch consensus target price for one company_id.""" resp = await httpx_client.get(url=f"/estimates/consensus_target_price/{company_id}") + resp.raise_for_status() return SingleResultResp[ConsensusTargetPrice].model_validate(resp.json()) @@ -373,4 +375,5 @@ async def fetch_analyst_recommendations_from_company_id( ) -> SingleResultResp[AnalystRecommendations]: """Fetch analyst recommendations for one company_id.""" resp = await httpx_client.get(url=f"/estimates/analyst_recommendations/{company_id}") + resp.raise_for_status() return SingleResultResp[AnalystRecommendations].model_validate(resp.json()) diff --git a/kfinance/domains/line_items/line_item_tools.py b/kfinance/domains/line_items/line_item_tools.py index d5257c4..0f26092 100644 --- a/kfinance/domains/line_items/line_item_tools.py +++ b/kfinance/domains/line_items/line_item_tools.py @@ -331,5 +331,6 @@ async def fetch_line_item_from_company_ids( params["num_periods_back"] = num_periods_back resp = await httpx_client.post(url="/line_item/", json=params) + resp.raise_for_status() return PostResponse[LineItemResp].model_validate(resp.json()) diff --git a/kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py b/kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py index 00fc974..aeddd21 100644 --- a/kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +++ b/kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py @@ -53,7 +53,7 @@ async def _arun(self, identifiers: list[str]) -> GetMergersFromIdentifiersResp: class GetMergerInfoFromTransactionIdArgs(BaseModel): - transaction_id: int | None = Field(description="The ID of the transaction.") + transaction_id: int = Field(description="The ID of the transaction.") class GetMergerInfoFromTransactionId(KfinanceTool): @@ -88,7 +88,7 @@ async def _arun(self, transaction_id: int) -> MergerInfo: class GetAdvisorsForCompanyInTransactionFromIdentifierArgs(ToolArgsWithIdentifier): - transaction_id: int | None = Field(description="The ID of the merger.") + transaction_id: int = Field(description="The ID of the merger.") class GetAdvisorsForCompanyInTransactionFromIdentifierResp(ToolRespWithErrors): @@ -204,6 +204,7 @@ async def get_advisors_for_company_in_transaction_from_identifier( # Fetch advisors for this company in the transaction url = f"/merger/info/{transaction_id}/advisors/{company_id}" resp = await httpx_client.get(url=url) + resp.raise_for_status() response_data = resp.json() advisors_response: list[AdvisorResp] = [] diff --git a/kfinance/domains/prices/price_tools.py b/kfinance/domains/prices/price_tools.py index fe64c61..aa701ab 100644 --- a/kfinance/domains/prices/price_tools.py +++ b/kfinance/domains/prices/price_tools.py @@ -172,6 +172,7 @@ async def fetch_price_history_from_trading_item_id( url = f"/pricing/{trading_item_id}/{start_date_str}/{end_date_str}/{periodicity.value}/{adjusted_str}" resp = await httpx_client.get(url=url) + resp.raise_for_status() return PriceHistory.model_validate(resp.json()) diff --git a/kfinance/domains/rounds_of_funding/rounds_of_funding_tools.py b/kfinance/domains/rounds_of_funding/rounds_of_funding_tools.py index 9770ef1..eae6935 100644 --- a/kfinance/domains/rounds_of_funding/rounds_of_funding_tools.py +++ b/kfinance/domains/rounds_of_funding/rounds_of_funding_tools.py @@ -402,11 +402,11 @@ async def get_rounds_of_funding_info_from_transaction_ids( await batch_execute_async_tasks(tasks=tasks) + errors: list[str] = [] round_of_info_responses: dict[int, RoundOfFundingInfo] = dict() for task in tasks: if task.error: - # For now, skip errors in individual round info fetches - continue + errors.append(f"transaction_id {task.result_key}: {task.error}") else: round_of_info_responses[task.result_key] = task.result @@ -464,7 +464,7 @@ async def get_rounds_of_funding_info_from_transaction_ids( return GetRoundsOfFundingInfoFromTransactionIdsResp( results=round_of_info_with_advisors, - errors=[], # Individual API failures would be captured in batch execution + errors=errors, ) diff --git a/kfinance/domains/rounds_of_funding/tests/test_rounds_of_funding_tools.py b/kfinance/domains/rounds_of_funding/tests/test_rounds_of_funding_tools.py index 915d93e..f662f61 100644 --- a/kfinance/domains/rounds_of_funding/tests/test_rounds_of_funding_tools.py +++ b/kfinance/domains/rounds_of_funding/tests/test_rounds_of_funding_tools.py @@ -485,8 +485,7 @@ async def test_get_rounds_of_funding_info_http_404( ) -> None: """ WHEN the server returns a 404 for a non-existent transaction_id - THEN the result contains no data for that transaction and no errors are raised - note: task errors are purposely ignored in get_rounds_of_funding_info_from_transaction_ids + THEN the result contains no data for that transaction and an error is reported """ transaction_id = 999999 @@ -498,7 +497,7 @@ async def test_get_rounds_of_funding_info_http_404( expected_result = GetRoundsOfFundingInfoFromTransactionIdsResp( results={}, - errors=[], + errors=[f"transaction_id {transaction_id}: No result found for {transaction_id}."], ) result = await get_rounds_of_funding_info_from_transaction_ids( diff --git a/kfinance/domains/segments/segment_tools.py b/kfinance/domains/segments/segment_tools.py index 21e24a7..c297a8e 100644 --- a/kfinance/domains/segments/segment_tools.py +++ b/kfinance/domains/segments/segment_tools.py @@ -219,5 +219,6 @@ async def fetch_segments_from_company_ids( url = "/segments/" resp = await httpx_client.post(url=url, json=payload) + resp.raise_for_status() return PostResponse[SegmentsResp].model_validate(resp.json()) diff --git a/kfinance/domains/statements/statement_tools.py b/kfinance/domains/statements/statement_tools.py index d344996..ea2272e 100644 --- a/kfinance/domains/statements/statement_tools.py +++ b/kfinance/domains/statements/statement_tools.py @@ -244,5 +244,6 @@ async def fetch_statements_from_company_ids( url = "/statements/" resp = await httpx_client.post(url=url, json=payload) + resp.raise_for_status() return PostResponse[StatementsResp].model_validate(resp.json()) diff --git a/kfinance/integrations/tool_calling/tool_calling_models.py b/kfinance/integrations/tool_calling/tool_calling_models.py index 35da53b..0a96e65 100644 --- a/kfinance/integrations/tool_calling/tool_calling_models.py +++ b/kfinance/integrations/tool_calling/tool_calling_models.py @@ -2,6 +2,7 @@ from typing import Annotated, Any, Callable, Coroutine, Dict, Literal, Type from asyncer import syncify +from httpx import HTTPStatusError from langchain_core.tools import BaseTool from pydantic import ( BaseModel, @@ -16,6 +17,11 @@ from kfinance.httpx_utils import KfinanceHttpxClient +def _sanitize_http_error(e: HTTPStatusError) -> str: + """Return the response body from an HTTPStatusError.""" + return f"{e.response.status_code}: {e.response.text}" + + class KfinanceTool(BaseTool): """KfinanceTool is a langchain base tool with a kfinance Client. @@ -62,7 +68,10 @@ async def arun_without_langchain(self, *args: Any, **kwargs: Any) -> dict: args_dict = args_model.model_dump() # Only pass params included in the LLM generated kwargs. args_dict = {k: v for k, v in args_dict.items() if k in kwargs} - result_model = await self._arun(**args_dict) + try: + result_model = await self._arun(**args_dict) + except HTTPStatusError as e: + raise Exception(_sanitize_http_error(e)) from None return result_model.model_dump(mode="json", exclude_none=True) async def run_with_endpoint_tracking(self, *args: Any, **kwargs: Any) -> Any: @@ -75,7 +84,10 @@ async def run_with_endpoint_tracking(self, *args: Any, **kwargs: Any) -> Any: args_model = self.args_schema.model_validate(kwargs) args_dict = args_model.model_dump() args_dict = {k: v for k, v in args_dict.items() if k in kwargs} - result_model = await self._arun(**args_dict) + try: + result_model = await self._arun(**args_dict) + except HTTPStatusError as e: + raise Exception(_sanitize_http_error(e)) from None # After completion of tool data fetching and within the endpoint_tracker context manager scope, # dequeue the endpoint_tracker_queue @@ -133,7 +145,8 @@ class ToolArgsWithIdentifiers(BaseModel): """ identifiers: list[str] = Field( - description="The identifiers, which can be a list of ticker symbols, ISINs, or CUSIPs, or company_ids" + min_length=1, + description="The identifiers, which can be a list of ticker symbols, ISINs, or CUSIPs, or company_ids", )