Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
96f97bb
Add consensus target price and analyst recommendation tools (#108)
michellekeoy Mar 17, 2026
30317f9
Update estimates tools to gracefully handle errors
matthew-rosen-12 Mar 19, 2026
93070c3
remove duplicate models
matthew-rosen-12 Mar 19, 2026
6c320d9
clean
matthew-rosen-12 Mar 19, 2026
cfa8f8e
lint
matthew-rosen-12 Mar 19, 2026
e70cc18
clean
matthew-rosen-12 Mar 19, 2026
f460392
clean
matthew-rosen-12 Mar 19, 2026
e311106
clean
matthew-rosen-12 Mar 19, 2026
26c58ca
docstrings
matthew-rosen-12 Mar 19, 2026
41cf30f
warnings
matthew-rosen-12 Mar 19, 2026
f81ef54
docs
matthew-rosen-12 Mar 19, 2026
fe3e8a9
clean
matthew-rosen-12 Mar 19, 2026
f29e41f
fix imports
matthew-rosen-12 Mar 19, 2026
a9335dc
lint
matthew-rosen-12 Mar 19, 2026
7addb63
fix other error handling
matthew-rosen-12 Mar 23, 2026
d892587
ammend tests
matthew-rosen-12 Mar 24, 2026
782354e
update test
matthew-rosen-12 Mar 24, 2026
c6dc3a5
update changelog
matthew-rosen-12 Mar 24, 2026
6315db6
catch 400 errors in task
matthew-rosen-12 Mar 25, 2026
16d26d4
Merge branch 'main' into mhr.estimates.errors
matthew-rosen-12 Mar 25, 2026
e35d636
use generic for single result response
matthew-rosen-12 Mar 30, 2026
c818bce
Merge branch 'mhr.estimates.errors' of github.com:kensho-technologies…
matthew-rosen-12 Mar 30, 2026
92dd41a
update comment
matthew-rosen-12 Mar 30, 2026
972de33
lint
matthew-rosen-12 Mar 30, 2026
b52b25d
[kfinance] handle more errors gracefully
matthew-rosen-12 Mar 31, 2026
73ed750
merge
matthew-rosen-12 Mar 31, 2026
b487fa2
clean
matthew-rosen-12 Mar 31, 2026
c21190f
clean
matthew-rosen-12 Mar 31, 2026
de99fb0
revert
matthew-rosen-12 Apr 2, 2026
27d5f65
lint
matthew-rosen-12 Apr 2, 2026
26d4c54
add tests
matthew-rosen-12 Apr 2, 2026
a4b3eb8
simplify santization
matthew-rosen-12 Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions kfinance/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## v5.1.4
- Update tools to gracefully handle more errors

## v5.1.3
- Bump fastmcp and langchain dependencies

Expand Down
1 change: 1 addition & 0 deletions kfinance/client/id_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 5 additions & 1 deletion kfinance/domains/companies/company_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
1 change: 1 addition & 0 deletions kfinance/domains/earnings/earning_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions kfinance/domains/estimates/estimates_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())


Expand Down Expand Up @@ -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())


Expand Down Expand Up @@ -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())
1 change: 1 addition & 0 deletions kfinance/domains/line_items/line_item_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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] = []
Expand Down
1 change: 1 addition & 0 deletions kfinance/domains/prices/price_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())


Expand Down
6 changes: 3 additions & 3 deletions kfinance/domains/rounds_of_funding/rounds_of_funding_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions kfinance/domains/segments/segment_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
1 change: 1 addition & 0 deletions kfinance/domains/statements/statement_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
19 changes: 16 additions & 3 deletions kfinance/integrations/tool_calling/tool_calling_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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",
)


Expand Down
Loading