kensho-kfinance 2.9.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kensho-kfinance might be problematic. Click here for more details.
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.1.dist-info}/METADATA +1 -1
- kensho_kfinance-3.0.1.dist-info/RECORD +111 -0
- kfinance/CHANGELOG.md +6 -0
- kfinance/__init__.py +1 -0
- kfinance/client/README.md +9 -0
- kfinance/{batch_request_handling.py → client/batch_request_handling.py} +63 -27
- kfinance/{fetch.py → client/fetch.py} +23 -29
- kfinance/{kfinance.py → client/kfinance.py} +37 -33
- kfinance/{meta_classes.py → client/meta_classes.py} +26 -35
- kfinance/{decimal_with_unit.py → client/models/decimal_with_unit.py} +1 -1
- kfinance/{tests → client/models/tests}/test_decimal_with_unit.py +1 -1
- kfinance/client/permission_models.py +16 -0
- kfinance/client/tests/__init__.py +0 -0
- kfinance/{tests → client/tests}/test_batch_requests.py +8 -6
- kfinance/{tests → client/tests}/test_client.py +25 -19
- kfinance/{tests → client/tests}/test_fetch.py +11 -29
- kfinance/{tests → client/tests}/test_group_objects.py +1 -1
- kfinance/{tests → client/tests}/test_objects.py +33 -29
- kfinance/{tests/conftest.py → conftest.py} +14 -2
- kfinance/domains/README.md +14 -0
- kfinance/domains/__init__.py +0 -0
- kfinance/domains/business_relationships/__init__.py +0 -0
- kfinance/{models → domains/business_relationships}/business_relationship_models.py +10 -0
- kfinance/domains/business_relationships/business_relationship_tools.py +74 -0
- kfinance/domains/business_relationships/tests/__init__.py +0 -0
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +55 -0
- kfinance/domains/capitalizations/__init__.py +0 -0
- kfinance/{models → domains/capitalizations}/capitalization_models.py +24 -17
- kfinance/domains/capitalizations/capitalization_tools.py +89 -0
- kfinance/domains/capitalizations/tests/__init__.py +0 -0
- kfinance/{tests/test_models → domains/capitalizations/tests}/test_capitalization_models.py +8 -10
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +85 -0
- kfinance/domains/companies/__init__.py +0 -0
- kfinance/domains/companies/company_identifiers.py +175 -0
- kfinance/domains/companies/company_models.py +27 -0
- kfinance/domains/companies/company_tools.py +66 -0
- kfinance/domains/companies/tests/__init__.py +0 -0
- kfinance/domains/companies/tests/test_company_tools.py +26 -0
- kfinance/domains/competitors/__init__.py +0 -0
- kfinance/{models → domains/competitors}/competitor_models.py +7 -0
- kfinance/domains/competitors/competitor_tools.py +62 -0
- kfinance/domains/competitors/tests/__init__.py +0 -0
- kfinance/domains/competitors/tests/test_competitor_tools.py +45 -0
- kfinance/domains/cusip_and_isin/__init__.py +0 -0
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +80 -0
- kfinance/domains/cusip_and_isin/tests/__init__.py +0 -0
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +57 -0
- kfinance/domains/earnings/__init__.py +0 -0
- kfinance/domains/earnings/earning_models.py +41 -0
- kfinance/domains/earnings/earning_tools.py +174 -0
- kfinance/domains/earnings/tests/__init__.py +0 -0
- kfinance/domains/earnings/tests/test_earnings_tools.py +195 -0
- kfinance/domains/line_items/__init__.py +0 -0
- kfinance/domains/line_items/line_item_tools.py +117 -0
- kfinance/domains/line_items/tests/__init__.py +0 -0
- kfinance/domains/line_items/tests/test_line_item_tools.py +86 -0
- kfinance/domains/mergers_and_acquisitions/__init__.py +0 -0
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +176 -0
- kfinance/domains/mergers_and_acquisitions/tests/__init__.py +0 -0
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +124 -0
- kfinance/domains/prices/__init__.py +0 -0
- kfinance/{models → domains/prices}/price_models.py +1 -1
- kfinance/domains/prices/price_tools.py +165 -0
- kfinance/domains/prices/tests/__init__.py +0 -0
- kfinance/{tests/test_models → domains/prices/tests}/test_price_models.py +2 -2
- kfinance/domains/prices/tests/test_price_tools.py +141 -0
- kfinance/domains/segments/__init__.py +0 -0
- kfinance/domains/segments/segment_tools.py +91 -0
- kfinance/domains/segments/tests/__init__.py +0 -0
- kfinance/domains/segments/tests/test_segment_tools.py +80 -0
- kfinance/domains/statements/__init__.py +0 -0
- kfinance/domains/statements/statement_tools.py +116 -0
- kfinance/domains/statements/tests/__init__.py +0 -0
- kfinance/domains/statements/tests/test_statement_tools.py +73 -0
- kfinance/integrations/README.md +8 -0
- kfinance/integrations/__init__.py +0 -0
- kfinance/integrations/mcp/__init__.py +0 -0
- kfinance/{mcp.py → integrations/mcp/mcp.py} +2 -2
- kfinance/integrations/tests/__init__.py +0 -0
- kfinance/{tests → integrations/tests}/test_example_notebook.py +4 -4
- kfinance/{tool_calling → integrations/tool_calling}/README.md +2 -2
- kfinance/integrations/tool_calling/__init__.py +0 -0
- kfinance/integrations/tool_calling/all_tools.py +55 -0
- kfinance/{tool_calling → integrations/tool_calling}/prompts.py +3 -2
- kfinance/integrations/tool_calling/static_tools/README.md +4 -0
- kfinance/integrations/tool_calling/static_tools/__init__.py +0 -0
- kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_latest.py +3 -3
- kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_n_quarters_ago.py +3 -3
- kfinance/integrations/tool_calling/static_tools/tests/__init__.py +0 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +30 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +24 -0
- kfinance/integrations/tool_calling/tests/__init__.py +0 -0
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +69 -0
- kfinance/{tool_calling/shared_models.py → integrations/tool_calling/tool_calling_models.py} +37 -7
- kfinance/models/permission_models.py +1 -0
- kfinance/version.py +2 -2
- kensho_kfinance-2.9.0.dist-info/RECORD +0 -70
- kfinance/models/id_models.py +0 -7
- kfinance/prompt.py +0 -526
- kfinance/pydantic_models.py +0 -33
- kfinance/tests/test_tools.py +0 -804
- kfinance/tool_calling/__init__.py +0 -53
- kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +0 -42
- kfinance/tool_calling/get_business_relationship_from_identifier.py +0 -30
- kfinance/tool_calling/get_capitalization_from_identifier.py +0 -35
- kfinance/tool_calling/get_competitors_from_identifier.py +0 -25
- kfinance/tool_calling/get_cusip_from_ticker.py +0 -20
- kfinance/tool_calling/get_earnings.py +0 -33
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +0 -48
- kfinance/tool_calling/get_financial_statement_from_identifier.py +0 -44
- kfinance/tool_calling/get_history_metadata_from_identifier.py +0 -17
- kfinance/tool_calling/get_info_from_identifier.py +0 -16
- kfinance/tool_calling/get_isin_from_ticker.py +0 -20
- kfinance/tool_calling/get_latest_earnings.py +0 -30
- kfinance/tool_calling/get_merger_info_from_transaction_id.py +0 -69
- kfinance/tool_calling/get_mergers_from_identifier.py +0 -44
- kfinance/tool_calling/get_next_earnings.py +0 -30
- kfinance/tool_calling/get_prices_from_identifier.py +0 -46
- kfinance/tool_calling/get_segments_from_identifier.py +0 -44
- kfinance/tool_calling/get_transcript.py +0 -23
- kfinance/tool_calling/resolve_identifier.py +0 -18
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.1.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.1.dist-info}/top_level.txt +0 -0
- /kfinance/{models → client}/__init__.py +0 -0
- /kfinance/{models → client}/industry_models.py +0 -0
- /kfinance/{tests → client/models}/__init__.py +0 -0
- /kfinance/{models → client/models}/currency_models.py +0 -0
- /kfinance/{models → client/models}/date_and_period_models.py +0 -0
- /kfinance/{tests/test_models → client/models/tests}/__init__.py +0 -0
- /kfinance/{server_thread.py → client/server_thread.py} +0 -0
- /kfinance/{models → domains/line_items}/line_item_models.py +0 -0
- /kfinance/{models → domains/segments}/segment_models.py +0 -0
- /kfinance/{models → domains/statements}/statement_models.py +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Literal, Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
6
|
+
from kfinance.client.models.date_and_period_models import PeriodType
|
|
7
|
+
from kfinance.client.permission_models import Permission
|
|
8
|
+
from kfinance.domains.companies.company_identifiers import (
|
|
9
|
+
fetch_company_ids_from_identifiers,
|
|
10
|
+
parse_identifiers,
|
|
11
|
+
)
|
|
12
|
+
from kfinance.domains.segments.segment_models import SegmentType
|
|
13
|
+
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
14
|
+
KfinanceTool,
|
|
15
|
+
ToolArgsWithIdentifiers,
|
|
16
|
+
ValidQuarter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GetSegmentsFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
21
|
+
# no description because the description for enum fields comes from the enum docstring.
|
|
22
|
+
segment_type: SegmentType
|
|
23
|
+
period_type: PeriodType | None = Field(default=None, description="The period type")
|
|
24
|
+
start_year: int | None = Field(default=None, description="The starting year for the data range")
|
|
25
|
+
end_year: int | None = Field(default=None, description="The ending year for the data range")
|
|
26
|
+
start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
|
|
27
|
+
end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GetSegmentsFromIdentifiers(KfinanceTool):
|
|
31
|
+
name: str = "get_segments_from_identifiers"
|
|
32
|
+
description: str = "Get the templated segments associated with a list of identifiers."
|
|
33
|
+
args_schema: Type[BaseModel] = GetSegmentsFromIdentifiersArgs
|
|
34
|
+
accepted_permissions: set[Permission] | None = {Permission.SegmentsPermission}
|
|
35
|
+
|
|
36
|
+
def _run(
|
|
37
|
+
self,
|
|
38
|
+
identifiers: list[str],
|
|
39
|
+
segment_type: SegmentType,
|
|
40
|
+
period_type: PeriodType | None = None,
|
|
41
|
+
start_year: int | None = None,
|
|
42
|
+
end_year: int | None = None,
|
|
43
|
+
start_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
44
|
+
end_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
45
|
+
) -> dict:
|
|
46
|
+
api_client = self.kfinance_client.kfinance_api_client
|
|
47
|
+
parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
|
|
48
|
+
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
49
|
+
identifiers=parsed_identifiers, api_client=api_client
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
tasks = [
|
|
53
|
+
Task(
|
|
54
|
+
func=api_client.fetch_segments,
|
|
55
|
+
kwargs=dict(
|
|
56
|
+
company_id=company_id,
|
|
57
|
+
segment_type=segment_type,
|
|
58
|
+
period_type=period_type,
|
|
59
|
+
start_year=start_year,
|
|
60
|
+
end_year=end_year,
|
|
61
|
+
start_quarter=start_quarter,
|
|
62
|
+
end_quarter=end_quarter,
|
|
63
|
+
),
|
|
64
|
+
result_key=identifier,
|
|
65
|
+
)
|
|
66
|
+
for identifier, company_id in identifiers_to_company_ids.items()
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
segments_responses = process_tasks_in_thread_pool_executor(
|
|
70
|
+
api_client=api_client, tasks=tasks
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# If no date and multiple companies, only return the most recent value.
|
|
74
|
+
# By default, we return 5 years of data, which can be too much when
|
|
75
|
+
# returning data for many companies.
|
|
76
|
+
if (
|
|
77
|
+
start_year is None
|
|
78
|
+
and end_year is None
|
|
79
|
+
and start_quarter is None
|
|
80
|
+
and end_quarter is None
|
|
81
|
+
and len(identifiers) > 1
|
|
82
|
+
):
|
|
83
|
+
for segments_response in segments_responses.values():
|
|
84
|
+
most_recent_year = max(segments_response["segments"].keys())
|
|
85
|
+
most_recent_year_data = segments_response["segments"][most_recent_year]
|
|
86
|
+
segments_response["segments"] = {most_recent_year: most_recent_year_data}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
str(identifier): segments["segments"]
|
|
90
|
+
for identifier, segments in segments_responses.items()
|
|
91
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from requests_mock import Mocker
|
|
2
|
+
|
|
3
|
+
from kfinance.client.kfinance import Client
|
|
4
|
+
from kfinance.conftest import SPGI_COMPANY_ID
|
|
5
|
+
from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
|
|
6
|
+
from kfinance.domains.segments.segment_models import SegmentType
|
|
7
|
+
from kfinance.domains.segments.segment_tools import (
|
|
8
|
+
GetSegmentsFromIdentifiers,
|
|
9
|
+
GetSegmentsFromIdentifiersArgs,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestGetSegmentsFromIdentifier:
|
|
14
|
+
segments_response = {
|
|
15
|
+
"segments": {
|
|
16
|
+
"2020": {
|
|
17
|
+
"Commodity Insights": {
|
|
18
|
+
"CAPEX": -7000000.0,
|
|
19
|
+
"D&A": 17000000.0,
|
|
20
|
+
},
|
|
21
|
+
"Unallocated Assets Held for Sale": None,
|
|
22
|
+
},
|
|
23
|
+
"2021": {
|
|
24
|
+
"Commodity Insights": {
|
|
25
|
+
"CAPEX": -2000000.0,
|
|
26
|
+
"D&A": 12000000.0,
|
|
27
|
+
},
|
|
28
|
+
"Unallocated Assets Held for Sale": {"Total Assets": 321000000.0},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def test_get_segments_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
34
|
+
"""
|
|
35
|
+
GIVEN the GetSegmentsFromIdentifier tool
|
|
36
|
+
WHEN we request the SPGI business segment
|
|
37
|
+
THEN we get back the SPGI business segment
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
requests_mock.get(
|
|
41
|
+
url=f"https://kfinance.kensho.com/api/v1/segments/{SPGI_COMPANY_ID}/business/none/none/none/none/none",
|
|
42
|
+
# truncated from the original API response
|
|
43
|
+
json=self.segments_response,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expected_response = {"SPGI": self.segments_response["segments"]}
|
|
47
|
+
|
|
48
|
+
tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
|
|
49
|
+
args = GetSegmentsFromIdentifiersArgs(
|
|
50
|
+
identifiers=["SPGI"], segment_type=SegmentType.business
|
|
51
|
+
)
|
|
52
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
53
|
+
assert response == expected_response
|
|
54
|
+
|
|
55
|
+
def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
56
|
+
"""
|
|
57
|
+
GIVEN the GetFinancialLineItemFromIdentifiers tool
|
|
58
|
+
WHEN we request most recent statement for multiple companies
|
|
59
|
+
THEN we only get back the most recent statement for each company
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
company_ids = [1, 2]
|
|
63
|
+
expected_response = {
|
|
64
|
+
"C_1": {"2021": self.segments_response["segments"]["2021"]},
|
|
65
|
+
"C_2": {"2021": self.segments_response["segments"]["2021"]},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for company_id in company_ids:
|
|
69
|
+
requests_mock.get(
|
|
70
|
+
url=f"https://kfinance.kensho.com/api/v1/segments/{company_id}/business/none/none/none/none/none",
|
|
71
|
+
json=self.segments_response,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
|
|
75
|
+
args = GetSegmentsFromIdentifiersArgs(
|
|
76
|
+
identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
|
|
77
|
+
segment_type=SegmentType.business,
|
|
78
|
+
)
|
|
79
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
80
|
+
assert response == expected_response
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
from typing import Literal, Type
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
9
|
+
from kfinance.client.models.date_and_period_models import PeriodType
|
|
10
|
+
from kfinance.client.permission_models import Permission
|
|
11
|
+
from kfinance.domains.companies.company_identifiers import (
|
|
12
|
+
fetch_company_ids_from_identifiers,
|
|
13
|
+
parse_identifiers,
|
|
14
|
+
)
|
|
15
|
+
from kfinance.domains.statements.statement_models import StatementType
|
|
16
|
+
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
17
|
+
KfinanceTool,
|
|
18
|
+
ToolArgsWithIdentifiers,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GetFinancialStatementFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
23
|
+
# no description because the description for enum fields comes from the enum docstring.
|
|
24
|
+
statement: StatementType
|
|
25
|
+
period_type: PeriodType | None = Field(default=None, description="The period type")
|
|
26
|
+
start_year: int | None = Field(default=None, description="The starting year for the data range")
|
|
27
|
+
end_year: int | None = Field(default=None, description="The ending year for the data range")
|
|
28
|
+
start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
|
|
29
|
+
end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GetFinancialStatementFromIdentifiers(KfinanceTool):
|
|
33
|
+
name: str = "get_financial_statement_from_identifiers"
|
|
34
|
+
description: str = dedent("""
|
|
35
|
+
Get a financial statement associated with a group of identifiers.
|
|
36
|
+
|
|
37
|
+
- To fetch the most recent value for the statement, leave start_year, start_quarter, end_year, and end_quarter as None.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
Query: "Fetch the balance sheets of BAC and GS for 2024"
|
|
41
|
+
Function: get_financial_statement_from_company_ids(identifiers=["BAC", "GS"], statement=StatementType.balance_sheet, start_year=2024, end_year=2024)
|
|
42
|
+
""").strip()
|
|
43
|
+
args_schema: Type[BaseModel] = GetFinancialStatementFromIdentifiersArgs
|
|
44
|
+
accepted_permissions: set[Permission] | None = {
|
|
45
|
+
Permission.StatementsPermission,
|
|
46
|
+
Permission.PrivateCompanyFinancialsPermission,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def _run(
|
|
50
|
+
self,
|
|
51
|
+
identifiers: list[str],
|
|
52
|
+
statement: StatementType,
|
|
53
|
+
period_type: PeriodType | None = None,
|
|
54
|
+
start_year: int | None = None,
|
|
55
|
+
end_year: int | None = None,
|
|
56
|
+
start_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
57
|
+
end_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Sample response:
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
'SPGI': {
|
|
63
|
+
'Revenues': {'2020': 7442000000.0, '2021': 8243000000.0},
|
|
64
|
+
'Total Revenues': {'2020': 7442000000.0, '2021': 8243000000.0}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
api_client = self.kfinance_client.kfinance_api_client
|
|
69
|
+
parsed_identifiers = parse_identifiers(identifiers, api_client=api_client)
|
|
70
|
+
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
71
|
+
identifiers=parsed_identifiers, api_client=api_client
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
tasks = [
|
|
75
|
+
Task(
|
|
76
|
+
func=api_client.fetch_statement,
|
|
77
|
+
kwargs=dict(
|
|
78
|
+
company_id=company_id,
|
|
79
|
+
statement_type=statement.value,
|
|
80
|
+
period_type=period_type,
|
|
81
|
+
start_year=start_year,
|
|
82
|
+
end_year=end_year,
|
|
83
|
+
start_quarter=start_quarter,
|
|
84
|
+
end_quarter=end_quarter,
|
|
85
|
+
),
|
|
86
|
+
result_key=identifier,
|
|
87
|
+
)
|
|
88
|
+
for identifier, company_id in identifiers_to_company_ids.items()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
statement_responses = process_tasks_in_thread_pool_executor(
|
|
92
|
+
api_client=api_client, tasks=tasks
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
output = dict()
|
|
96
|
+
for identifier, result in statement_responses.items():
|
|
97
|
+
df = (
|
|
98
|
+
pd.DataFrame(result["statements"])
|
|
99
|
+
.apply(pd.to_numeric)
|
|
100
|
+
.replace(np.nan, None)
|
|
101
|
+
.transpose()
|
|
102
|
+
)
|
|
103
|
+
# If no date and multiple companies, only return the most recent value.
|
|
104
|
+
# By default, we return 5 years of data, which can be too much when
|
|
105
|
+
# returning data for many companies.
|
|
106
|
+
if (
|
|
107
|
+
start_year is None
|
|
108
|
+
and end_year is None
|
|
109
|
+
and start_quarter is None
|
|
110
|
+
and end_quarter is None
|
|
111
|
+
and len(identifiers) > 1
|
|
112
|
+
):
|
|
113
|
+
df = df.tail(1)
|
|
114
|
+
output[str(identifier)] = df.to_dict()
|
|
115
|
+
|
|
116
|
+
return output
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from requests_mock import Mocker
|
|
2
|
+
|
|
3
|
+
from kfinance.client.kfinance import Client
|
|
4
|
+
from kfinance.conftest import SPGI_COMPANY_ID
|
|
5
|
+
from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
|
|
6
|
+
from kfinance.domains.statements.statement_models import StatementType
|
|
7
|
+
from kfinance.domains.statements.statement_tools import (
|
|
8
|
+
GetFinancialStatementFromIdentifiers,
|
|
9
|
+
GetFinancialStatementFromIdentifiersArgs,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestGetFinancialStatementFromIdentifiers:
|
|
14
|
+
statement_resp = {
|
|
15
|
+
"statements": {
|
|
16
|
+
"2020": {"Revenues": "7442000000.000000", "Total Revenues": "7442000000.000000"},
|
|
17
|
+
"2021": {"Revenues": "8243000000.000000", "Total Revenues": "8243000000.000000"},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def test_get_financial_statement_from_identifiers(
|
|
22
|
+
self, mock_client: Client, requests_mock: Mocker
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
GIVEN the GetFinancialLineItemFromIdentifiers tool
|
|
26
|
+
WHEN we request the SPGI income statement
|
|
27
|
+
THEN we get back the SPGI income statement
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
requests_mock.get(
|
|
31
|
+
url=f"https://kfinance.kensho.com/api/v1/statements/{SPGI_COMPANY_ID}/income_statement/none/none/none/none/none",
|
|
32
|
+
json=self.statement_resp,
|
|
33
|
+
)
|
|
34
|
+
expected_response = {
|
|
35
|
+
"SPGI": {
|
|
36
|
+
"Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
|
|
37
|
+
"Total Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
|
|
42
|
+
args = GetFinancialStatementFromIdentifiersArgs(
|
|
43
|
+
identifiers=["SPGI"], statement=StatementType.income_statement
|
|
44
|
+
)
|
|
45
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
46
|
+
assert response == expected_response
|
|
47
|
+
|
|
48
|
+
def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
49
|
+
"""
|
|
50
|
+
GIVEN the GetFinancialLineItemFromIdentifiers tool
|
|
51
|
+
WHEN we request most recent statement for multiple companies
|
|
52
|
+
THEN we only get back the most recent statement for each company
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
company_ids = [1, 2]
|
|
56
|
+
expected_response = {
|
|
57
|
+
"C_1": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
|
|
58
|
+
"C_2": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for company_id in company_ids:
|
|
62
|
+
requests_mock.get(
|
|
63
|
+
url=f"https://kfinance.kensho.com/api/v1/statements/{company_id}/income_statement/none/none/none/none/none",
|
|
64
|
+
json=self.statement_resp,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
|
|
68
|
+
args = GetFinancialStatementFromIdentifiersArgs(
|
|
69
|
+
identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
|
|
70
|
+
statement=StatementType.income_statement,
|
|
71
|
+
)
|
|
72
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
73
|
+
assert response == expected_response
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Integrations
|
|
2
|
+
|
|
3
|
+
Integrations holds integrations downstream from the main
|
|
4
|
+
Kfinance `Client`. This currently includes tool calling
|
|
5
|
+
and MCP. Note that only shared functionality for tool
|
|
6
|
+
calling should be put into the `tool_calling` directory.
|
|
7
|
+
The tools themselves should be defined in the relevant
|
|
8
|
+
`domains` sub directory.
|
|
File without changes
|
|
File without changes
|
|
@@ -6,8 +6,8 @@ from fastmcp.tools import FunctionTool
|
|
|
6
6
|
from fastmcp.utilities.logging import get_logger
|
|
7
7
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
8
8
|
|
|
9
|
-
from kfinance.kfinance import Client
|
|
10
|
-
from kfinance.tool_calling import KfinanceTool
|
|
9
|
+
from kfinance.client.kfinance import Client
|
|
10
|
+
from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
logger = get_logger(__name__)
|
|
File without changes
|
|
@@ -55,7 +55,7 @@ def test_run_notebook(jupyter_kernel_name: str):
|
|
|
55
55
|
# - mocks for all calls made by the client while executing the notebook
|
|
56
56
|
startup_cell_code = dedent("""
|
|
57
57
|
from datetime import datetime
|
|
58
|
-
from kfinance.kfinance import Client
|
|
58
|
+
from kfinance.client.kfinance import Client
|
|
59
59
|
kfinance_client = Client(refresh_token="foo")
|
|
60
60
|
api_client = kfinance_client.kfinance_api_client
|
|
61
61
|
# Set access token so that the client doesn't try to fetch it.
|
|
@@ -161,13 +161,13 @@ def test_run_notebook(jupyter_kernel_name: str):
|
|
|
161
161
|
)
|
|
162
162
|
# Mock out image_open so that we don't have to return an actual png.
|
|
163
163
|
from unittest.mock import MagicMock
|
|
164
|
-
import kfinance.kfinance
|
|
165
|
-
kfinance.kfinance.image_open = MagicMock()
|
|
164
|
+
import kfinance.client.kfinance
|
|
165
|
+
kfinance.client.kfinance.image_open = MagicMock()
|
|
166
166
|
""")
|
|
167
167
|
|
|
168
168
|
# Load the notebook
|
|
169
169
|
notebook_path = Path(
|
|
170
|
-
Path(__file__).parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
|
|
170
|
+
Path(__file__).parent.parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
|
|
171
171
|
)
|
|
172
172
|
with notebook_path.open() as f:
|
|
173
173
|
nb = nbformat.read(f, as_version=4)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The tools defined in this directory are intended for tool calling.
|
|
4
4
|
|
|
5
|
-
Each tool is a subclass of [KfinanceTool](
|
|
5
|
+
Each tool is a subclass of [KfinanceTool](tool_calling_models.py), which is
|
|
6
6
|
turn a subclass of `BaseTool` from langchain. We use langchain to convert these tools
|
|
7
7
|
into LLM-specific tool descriptions.
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ of the arguments.
|
|
|
14
14
|
- args_schema: A pydantic model defining the input schema for the function (more on that below)
|
|
15
15
|
- _run: the source code for the tool.
|
|
16
16
|
|
|
17
|
-
All new tools have to be added to the `ALL_TOOLS` [list](
|
|
17
|
+
All new tools have to be added to the `ALL_TOOLS` [list](all_tools.py).
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
When initializing a `KfinanceTool`, it's always required to pass in an initialized Kfinance
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from kfinance.domains.business_relationships.business_relationship_tools import (
|
|
2
|
+
GetBusinessRelationshipFromIdentifiers,
|
|
3
|
+
)
|
|
4
|
+
from kfinance.domains.capitalizations.capitalization_tools import GetCapitalizationFromIdentifiers
|
|
5
|
+
from kfinance.domains.companies.company_tools import GetInfoFromIdentifiers
|
|
6
|
+
from kfinance.domains.competitors.competitor_tools import GetCompetitorsFromIdentifiers
|
|
7
|
+
from kfinance.domains.cusip_and_isin.cusip_and_isin_tools import (
|
|
8
|
+
GetCusipFromIdentifiers,
|
|
9
|
+
GetIsinFromIdentifiers,
|
|
10
|
+
)
|
|
11
|
+
from kfinance.domains.earnings.earning_tools import (
|
|
12
|
+
GetEarningsFromIdentifiers,
|
|
13
|
+
GetLatestEarningsFromIdentifiers,
|
|
14
|
+
GetNextEarningsFromIdentifiers,
|
|
15
|
+
GetTranscriptFromKeyDevId,
|
|
16
|
+
)
|
|
17
|
+
from kfinance.domains.line_items.line_item_tools import GetFinancialLineItemFromIdentifiers
|
|
18
|
+
from kfinance.domains.prices.price_tools import GetPricesFromIdentifiers
|
|
19
|
+
from kfinance.domains.segments.segment_tools import GetSegmentsFromIdentifiers
|
|
20
|
+
from kfinance.domains.statements.statement_tools import GetFinancialStatementFromIdentifiers
|
|
21
|
+
from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
|
|
22
|
+
from kfinance.integrations.tool_calling.static_tools.get_n_quarters_ago import GetNQuartersAgo
|
|
23
|
+
from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# A list of all available tools
|
|
27
|
+
ALL_TOOLS: list[type[KfinanceTool]] = [
|
|
28
|
+
# Static / no API call tools
|
|
29
|
+
GetLatest,
|
|
30
|
+
GetNQuartersAgo,
|
|
31
|
+
# Business Relationships
|
|
32
|
+
GetBusinessRelationshipFromIdentifiers,
|
|
33
|
+
# Capitalizations
|
|
34
|
+
GetCapitalizationFromIdentifiers,
|
|
35
|
+
# Companies
|
|
36
|
+
GetInfoFromIdentifiers,
|
|
37
|
+
# Competitors
|
|
38
|
+
GetCompetitorsFromIdentifiers,
|
|
39
|
+
# Cusip and Isin
|
|
40
|
+
GetCusipFromIdentifiers,
|
|
41
|
+
GetIsinFromIdentifiers,
|
|
42
|
+
# Earnings
|
|
43
|
+
GetEarningsFromIdentifiers,
|
|
44
|
+
GetLatestEarningsFromIdentifiers,
|
|
45
|
+
GetNextEarningsFromIdentifiers,
|
|
46
|
+
GetTranscriptFromKeyDevId,
|
|
47
|
+
# Line Items
|
|
48
|
+
GetFinancialLineItemFromIdentifiers,
|
|
49
|
+
# Prices
|
|
50
|
+
GetPricesFromIdentifiers,
|
|
51
|
+
# Segments
|
|
52
|
+
GetSegmentsFromIdentifiers,
|
|
53
|
+
# Statements
|
|
54
|
+
GetFinancialStatementFromIdentifiers,
|
|
55
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from kfinance.
|
|
1
|
+
from kfinance.domains.line_items.line_item_tools import GetFinancialLineItemFromIdentifiers
|
|
2
|
+
from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
BASE_PROMPT = f"""
|
|
@@ -7,7 +8,7 @@ BASE_PROMPT = f"""
|
|
|
7
8
|
|
|
8
9
|
- Always use the `{GetLatest.model_fields["name"].default}` function when asked about the last or most recent quarter or
|
|
9
10
|
when the time is unspecified in the question.
|
|
10
|
-
- Try to use `{
|
|
11
|
+
- Try to use `{GetFinancialLineItemFromIdentifiers.model_fields["name"].default}` for questions about a company's
|
|
11
12
|
finances.
|
|
12
13
|
- If the tools do not respond with data that answers the question, then respond by saying that
|
|
13
14
|
you don't have the data available.
|
|
File without changes
|
|
@@ -2,9 +2,9 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.models.date_and_period_models import LatestPeriods
|
|
6
|
-
from kfinance.
|
|
7
|
-
from kfinance.tool_calling.
|
|
5
|
+
from kfinance.client.models.date_and_period_models import LatestPeriods
|
|
6
|
+
from kfinance.client.permission_models import Permission
|
|
7
|
+
from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class GetLatestArgs(BaseModel):
|
|
@@ -2,9 +2,9 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.models.date_and_period_models import YearAndQuarter
|
|
6
|
-
from kfinance.
|
|
7
|
-
from kfinance.tool_calling.
|
|
5
|
+
from kfinance.client.models.date_and_period_models import YearAndQuarter
|
|
6
|
+
from kfinance.client.permission_models import Permission
|
|
7
|
+
from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class GetNQuartersAgoArgs(BaseModel):
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import time_machine
|
|
4
|
+
|
|
5
|
+
from kfinance.client.kfinance import Client
|
|
6
|
+
from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest, GetLatestArgs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestGetLatest:
|
|
10
|
+
@time_machine.travel(datetime(2025, 1, 1, 12, tzinfo=datetime.now().astimezone().tzinfo))
|
|
11
|
+
def test_get_latest(self, mock_client: Client):
|
|
12
|
+
"""
|
|
13
|
+
GIVEN the GetLatest tool
|
|
14
|
+
WHEN request latest info
|
|
15
|
+
THEN we get back latest info
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
expected_resp = {
|
|
19
|
+
"annual": {"latest_year": 2024},
|
|
20
|
+
"now": {
|
|
21
|
+
"current_date": "2025-01-01",
|
|
22
|
+
"current_month": 1,
|
|
23
|
+
"current_quarter": 1,
|
|
24
|
+
"current_year": 2025,
|
|
25
|
+
},
|
|
26
|
+
"quarterly": {"latest_quarter": 4, "latest_year": 2024},
|
|
27
|
+
}
|
|
28
|
+
tool = GetLatest(kfinance_client=mock_client)
|
|
29
|
+
resp = tool.run(GetLatestArgs().model_dump(mode="json"))
|
|
30
|
+
assert resp == expected_resp
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import time_machine
|
|
4
|
+
|
|
5
|
+
from kfinance.client.kfinance import Client
|
|
6
|
+
from kfinance.integrations.tool_calling.static_tools.get_n_quarters_ago import (
|
|
7
|
+
GetNQuartersAgo,
|
|
8
|
+
GetNQuartersAgoArgs,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestGetNQuartersAgo:
|
|
13
|
+
@time_machine.travel(datetime(2025, 1, 1, 12, tzinfo=datetime.now().astimezone().tzinfo))
|
|
14
|
+
def test_get_n_quarters_ago(self, mock_client: Client):
|
|
15
|
+
"""
|
|
16
|
+
GIVEN the GetNQuartersAgo tool
|
|
17
|
+
WHEN we request 3 quarters ago
|
|
18
|
+
THEN we get back 3 quarters ago
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
expected_resp = {"quarter": 2, "year": 2024}
|
|
22
|
+
tool = GetNQuartersAgo(kfinance_client=mock_client)
|
|
23
|
+
resp = tool.run(GetNQuartersAgoArgs(n=3).model_dump(mode="json"))
|
|
24
|
+
assert resp == expected_resp
|
|
File without changes
|