kensho-kfinance 2.9.0__py3-none-any.whl → 3.0.0__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.0.dist-info}/METADATA +1 -1
- kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
- kfinance/CHANGELOG.md +3 -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/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 +114 -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 +113 -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/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.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.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/{models → client}/permission_models.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,41 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasPath, BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EarningsCall(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
key_dev_id: int
|
|
9
|
+
datetime: datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EarningsCallResp(BaseModel):
|
|
13
|
+
earnings_calls: list[EarningsCall] = Field(validation_alias=AliasPath("earnings"))
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def most_recent_earnings(self) -> EarningsCall | None:
|
|
17
|
+
"""Returns the most recent earnings call if available."""
|
|
18
|
+
|
|
19
|
+
past_earnings = [e for e in self.earnings_calls if e.datetime < datetime.now(timezone.utc)]
|
|
20
|
+
if past_earnings:
|
|
21
|
+
return max(past_earnings, key=lambda x: x.datetime)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def next_earnings(self) -> EarningsCall | None:
|
|
26
|
+
"""Returns the next earnings call if available."""
|
|
27
|
+
|
|
28
|
+
future_earnings = [
|
|
29
|
+
e for e in self.earnings_calls if e.datetime > datetime.now(timezone.utc)
|
|
30
|
+
]
|
|
31
|
+
if future_earnings:
|
|
32
|
+
return min(future_earnings, key=lambda x: x.datetime)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TranscriptComponent(BaseModel):
|
|
37
|
+
"""A transcript component with person name, text, and component type."""
|
|
38
|
+
|
|
39
|
+
person_name: str
|
|
40
|
+
text: str
|
|
41
|
+
component_type: str
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
7
|
+
from kfinance.client.fetch import KFinanceApiClient
|
|
8
|
+
from kfinance.client.permission_models import Permission
|
|
9
|
+
from kfinance.domains.companies.company_identifiers import (
|
|
10
|
+
CompanyIdentifier,
|
|
11
|
+
fetch_company_ids_from_identifiers,
|
|
12
|
+
parse_identifiers,
|
|
13
|
+
)
|
|
14
|
+
from kfinance.domains.earnings.earning_models import EarningsCallResp
|
|
15
|
+
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
16
|
+
KfinanceTool,
|
|
17
|
+
ToolArgsWithIdentifiers,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GetEarningsFromIdentifiers(KfinanceTool):
|
|
22
|
+
name: str = "get_earnings_from_identifiers"
|
|
23
|
+
description: str = dedent("""
|
|
24
|
+
Get all earnings for a list of identifiers.
|
|
25
|
+
|
|
26
|
+
Returns a list of dictionaries, with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier.
|
|
27
|
+
""").strip()
|
|
28
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
|
|
29
|
+
accepted_permissions: set[Permission] | None = {
|
|
30
|
+
Permission.EarningsPermission,
|
|
31
|
+
Permission.TranscriptsPermission,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def _run(self, identifiers: list[str]) -> dict:
|
|
35
|
+
"""Sample response:
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
'SPGI': [
|
|
39
|
+
{
|
|
40
|
+
'datetime': '2025-04-29T12:30:00Z',
|
|
41
|
+
'key_dev_id': 12346,
|
|
42
|
+
'name': 'SPGI Q1 2025 Earnings Call'
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
earnings_responses = get_earnings_from_identifiers(
|
|
49
|
+
identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
|
|
50
|
+
)
|
|
51
|
+
return {
|
|
52
|
+
str(identifier): earnings.model_dump(mode="json")["earnings_calls"]
|
|
53
|
+
for identifier, earnings in earnings_responses.items()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GetLatestEarningsFromIdentifiers(KfinanceTool):
|
|
58
|
+
name: str = "get_latest_earnings_from_identifiers"
|
|
59
|
+
description: str = dedent("""
|
|
60
|
+
Get the latest earnings for a list of identifiers.
|
|
61
|
+
|
|
62
|
+
Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier.
|
|
63
|
+
""").strip()
|
|
64
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
|
|
65
|
+
accepted_permissions: set[Permission] | None = {
|
|
66
|
+
Permission.EarningsPermission,
|
|
67
|
+
Permission.TranscriptsPermission,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def _run(self, identifiers: list[str]) -> dict:
|
|
71
|
+
"""Sample response:
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
'JPM': {
|
|
75
|
+
'datetime': '2025-04-29T12:30:00Z',
|
|
76
|
+
'key_dev_id': 12346,
|
|
77
|
+
'name': 'SPGI Q1 2025 Earnings Call'
|
|
78
|
+
},
|
|
79
|
+
'SPGI': 'No latest earnings available.'
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
earnings_responses = get_earnings_from_identifiers(
|
|
83
|
+
identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
|
|
84
|
+
)
|
|
85
|
+
output = {}
|
|
86
|
+
for identifier, earnings in earnings_responses.items():
|
|
87
|
+
most_recent_earnings = earnings.most_recent_earnings
|
|
88
|
+
if most_recent_earnings:
|
|
89
|
+
identifier_output: str | dict = most_recent_earnings.model_dump(mode="json")
|
|
90
|
+
else:
|
|
91
|
+
identifier_output = f"No latest earnings available."
|
|
92
|
+
output[str(identifier)] = identifier_output
|
|
93
|
+
return output
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GetNextEarningsFromIdentifiers(KfinanceTool):
|
|
97
|
+
name: str = "get_next_earnings_from_identifiers"
|
|
98
|
+
description: str = dedent("""
|
|
99
|
+
Get the next earnings for a given identifier.
|
|
100
|
+
|
|
101
|
+
Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier."
|
|
102
|
+
""").strip()
|
|
103
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
|
|
104
|
+
accepted_permissions: set[Permission] | None = {
|
|
105
|
+
Permission.EarningsPermission,
|
|
106
|
+
Permission.TranscriptsPermission,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def _run(self, identifiers: list[str]) -> dict:
|
|
110
|
+
"""Sample response:
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
'JPM': {
|
|
114
|
+
'datetime': '2025-04-29T12:30:00Z',
|
|
115
|
+
'key_dev_id': 12346,
|
|
116
|
+
'name': 'SPGI Q1 2025 Earnings Call'
|
|
117
|
+
},
|
|
118
|
+
'SPGI': 'No next earnings available.'
|
|
119
|
+
}
|
|
120
|
+
"""
|
|
121
|
+
earnings_responses = get_earnings_from_identifiers(
|
|
122
|
+
identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
|
|
123
|
+
)
|
|
124
|
+
output = {}
|
|
125
|
+
for identifier, earnings in earnings_responses.items():
|
|
126
|
+
next_earnings = earnings.next_earnings
|
|
127
|
+
if next_earnings:
|
|
128
|
+
identifier_output: str | dict = next_earnings.model_dump(mode="json")
|
|
129
|
+
else:
|
|
130
|
+
identifier_output = f"No next earnings available."
|
|
131
|
+
output[str(identifier)] = identifier_output
|
|
132
|
+
return output
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_earnings_from_identifiers(
|
|
136
|
+
identifiers: list[str], kfinance_api_client: KFinanceApiClient
|
|
137
|
+
) -> dict[CompanyIdentifier, EarningsCallResp]:
|
|
138
|
+
"""Return the earnings call response for all passed identifiers."""
|
|
139
|
+
|
|
140
|
+
parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=kfinance_api_client)
|
|
141
|
+
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
142
|
+
identifiers=parsed_identifiers, api_client=kfinance_api_client
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
tasks = [
|
|
146
|
+
Task(
|
|
147
|
+
func=kfinance_api_client.fetch_earnings,
|
|
148
|
+
kwargs=dict(company_id=company_id),
|
|
149
|
+
result_key=identifier,
|
|
150
|
+
)
|
|
151
|
+
for identifier, company_id in identifiers_to_company_ids.items()
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
earnings_responses = process_tasks_in_thread_pool_executor(
|
|
155
|
+
api_client=kfinance_api_client, tasks=tasks
|
|
156
|
+
)
|
|
157
|
+
return earnings_responses
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class GetTranscriptFromKeyDevIdArgs(BaseModel):
|
|
161
|
+
"""Tool argument with a key_dev_id."""
|
|
162
|
+
|
|
163
|
+
key_dev_id: int = Field(description="The key dev ID for the earnings call")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class GetTranscriptFromKeyDevId(KfinanceTool):
|
|
167
|
+
name: str = "get_transcript_from_key_dev_id"
|
|
168
|
+
description: str = "Get the raw transcript text for an earnings call by key dev ID."
|
|
169
|
+
args_schema: Type[BaseModel] = GetTranscriptFromKeyDevIdArgs
|
|
170
|
+
accepted_permissions: set[Permission] | None = {Permission.TranscriptsPermission}
|
|
171
|
+
|
|
172
|
+
def _run(self, key_dev_id: int) -> str:
|
|
173
|
+
transcript = self.kfinance_client.transcript(key_dev_id)
|
|
174
|
+
return transcript.raw
|
|
File without changes
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from requests_mock import Mocker
|
|
4
|
+
import time_machine
|
|
5
|
+
|
|
6
|
+
from kfinance.client.kfinance import Client
|
|
7
|
+
from kfinance.conftest import SPGI_COMPANY_ID
|
|
8
|
+
from kfinance.domains.earnings.earning_tools import (
|
|
9
|
+
GetEarningsFromIdentifiers,
|
|
10
|
+
GetLatestEarningsFromIdentifiers,
|
|
11
|
+
GetNextEarningsFromIdentifiers,
|
|
12
|
+
GetTranscriptFromKeyDevId,
|
|
13
|
+
GetTranscriptFromKeyDevIdArgs,
|
|
14
|
+
)
|
|
15
|
+
from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestGetEarnings:
|
|
19
|
+
earnings_response = {
|
|
20
|
+
"earnings": [
|
|
21
|
+
{
|
|
22
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
23
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
24
|
+
"key_dev_id": 12346,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
28
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
29
|
+
"key_dev_id": 12345,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def test_get_earnings_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
|
|
35
|
+
"""
|
|
36
|
+
GIVEN the GetEarnings tool
|
|
37
|
+
WHEN we request all earnings for SPGI
|
|
38
|
+
THEN we get back all SPGI earnings
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
requests_mock.get(
|
|
42
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
43
|
+
json=self.earnings_response,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expected_response = {
|
|
47
|
+
"SPGI": [
|
|
48
|
+
{
|
|
49
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
50
|
+
"key_dev_id": 12346,
|
|
51
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
55
|
+
"key_dev_id": 12345,
|
|
56
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
tool = GetEarningsFromIdentifiers(kfinance_client=mock_client)
|
|
62
|
+
response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
|
|
63
|
+
assert response == expected_response
|
|
64
|
+
|
|
65
|
+
@time_machine.travel(
|
|
66
|
+
datetime(
|
|
67
|
+
2025,
|
|
68
|
+
5,
|
|
69
|
+
1,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
def test_get_latest_earnings_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
|
|
73
|
+
"""
|
|
74
|
+
GIVEN the GetLatestEarnings tool
|
|
75
|
+
WHEN we request the latest earnings for SPGI
|
|
76
|
+
THEN we get back the latest SPGI earnings
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
requests_mock.get(
|
|
80
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
81
|
+
json=self.earnings_response,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expected_response = {
|
|
85
|
+
"SPGI": {
|
|
86
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
87
|
+
"key_dev_id": 12346,
|
|
88
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
tool = GetLatestEarningsFromIdentifiers(kfinance_client=mock_client)
|
|
93
|
+
response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
|
|
94
|
+
assert response == expected_response
|
|
95
|
+
|
|
96
|
+
def test_get_latest_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
97
|
+
"""
|
|
98
|
+
GIVEN the GetLatestEarnings tool
|
|
99
|
+
WHEN we request the latest earnings for a company with no data
|
|
100
|
+
THEN we get a `No latest earnings available` message.
|
|
101
|
+
"""
|
|
102
|
+
requests_mock.get(
|
|
103
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
104
|
+
json={"earnings": []},
|
|
105
|
+
)
|
|
106
|
+
expected_response = {"SPGI": "No latest earnings available."}
|
|
107
|
+
|
|
108
|
+
tool = GetLatestEarningsFromIdentifiers(kfinance_client=mock_client)
|
|
109
|
+
response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
|
|
110
|
+
assert response == expected_response
|
|
111
|
+
|
|
112
|
+
@time_machine.travel(
|
|
113
|
+
datetime(
|
|
114
|
+
2025,
|
|
115
|
+
4,
|
|
116
|
+
1,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
def test_get_next_earnings(self, requests_mock: Mocker, mock_client: Client):
|
|
120
|
+
"""
|
|
121
|
+
GIVEN the GetNextEarningsFromIdentifiers tool
|
|
122
|
+
WHEN we request the next earnings for SPGI
|
|
123
|
+
THEN we get back the next SPGI earnings
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
requests_mock.get(
|
|
127
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
128
|
+
json=self.earnings_response,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
expected_response = {
|
|
132
|
+
"SPGI": {
|
|
133
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
134
|
+
"key_dev_id": 12346,
|
|
135
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
tool = GetNextEarningsFromIdentifiers(kfinance_client=mock_client)
|
|
140
|
+
response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
|
|
141
|
+
assert response == expected_response
|
|
142
|
+
|
|
143
|
+
def test_get_next_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
144
|
+
"""
|
|
145
|
+
GIVEN the GetNextEarnings tool
|
|
146
|
+
WHEN we request the next earnings for a company with no data
|
|
147
|
+
THEN we get a `No next earnings available` message.
|
|
148
|
+
"""
|
|
149
|
+
earnings_data = {"earnings": []}
|
|
150
|
+
|
|
151
|
+
requests_mock.get(
|
|
152
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
153
|
+
json=earnings_data,
|
|
154
|
+
)
|
|
155
|
+
expected_response = {"SPGI": "No next earnings available."}
|
|
156
|
+
|
|
157
|
+
tool = GetNextEarningsFromIdentifiers(kfinance_client=mock_client)
|
|
158
|
+
response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
|
|
159
|
+
assert response == expected_response
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestGetTranscript:
|
|
163
|
+
def test_get_transcript(self, requests_mock: Mocker, mock_client: Client):
|
|
164
|
+
"""
|
|
165
|
+
GIVEN the GetTranscript tool
|
|
166
|
+
WHEN we request a transcript by key_dev_id
|
|
167
|
+
THEN we get back the transcript text
|
|
168
|
+
"""
|
|
169
|
+
transcript_data = {
|
|
170
|
+
"transcript": [
|
|
171
|
+
{
|
|
172
|
+
"person_name": "Operator",
|
|
173
|
+
"text": "Good morning, everyone.",
|
|
174
|
+
"component_type": "speech",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"person_name": "CEO",
|
|
178
|
+
"text": "Thank you for joining us today.",
|
|
179
|
+
"component_type": "speech",
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
requests_mock.get(
|
|
185
|
+
url="https://kfinance.kensho.com/api/v1/transcript/12345",
|
|
186
|
+
json=transcript_data,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expected_response = (
|
|
190
|
+
"Operator: Good morning, everyone.\n\nCEO: Thank you for joining us today."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
tool = GetTranscriptFromKeyDevId(kfinance_client=mock_client)
|
|
194
|
+
response = tool.run(GetTranscriptFromKeyDevIdArgs(key_dev_id=12345).model_dump(mode="json"))
|
|
195
|
+
assert response == expected_response
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
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.line_items.line_item_models import LINE_ITEM_NAMES_AND_ALIASES
|
|
16
|
+
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
17
|
+
KfinanceTool,
|
|
18
|
+
ToolArgsWithIdentifiers,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
23
|
+
# Note: mypy will not enforce this literal because of the type: ignore.
|
|
24
|
+
# But pydantic still uses the literal to check for allowed values and only includes
|
|
25
|
+
# allowed values in generated schemas.
|
|
26
|
+
line_item: Literal[tuple(LINE_ITEM_NAMES_AND_ALIASES)] = Field( # type: ignore[valid-type]
|
|
27
|
+
description="The type of financial line_item requested"
|
|
28
|
+
)
|
|
29
|
+
period_type: PeriodType | None = Field(default=None, description="The period type")
|
|
30
|
+
start_year: int | None = Field(default=None, description="The starting year for the data range")
|
|
31
|
+
end_year: int | None = Field(default=None, description="The ending year for the data range")
|
|
32
|
+
start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
|
|
33
|
+
end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GetFinancialLineItemFromIdentifiers(KfinanceTool):
|
|
37
|
+
name: str = "get_financial_line_item_from_identifiers"
|
|
38
|
+
description: str = dedent("""
|
|
39
|
+
Get the financial line item associated with a list of identifiers.
|
|
40
|
+
|
|
41
|
+
- When possible, pass multiple identifiers in a single call rather than making multiple calls.
|
|
42
|
+
- To fetch the most recent value for the line item, leave start_year, start_quarter, end_year, and end_quarter as None.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
Query: "What are the revenues of Lowe's and Home Depot?"
|
|
46
|
+
Function: get_financial_line_item_from_identifiers(line_item="revenue", company_ids=["LW", "HD"])
|
|
47
|
+
""").strip()
|
|
48
|
+
args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifiersArgs
|
|
49
|
+
accepted_permissions: set[Permission] | None = {Permission.StatementsPermission}
|
|
50
|
+
|
|
51
|
+
def _run(
|
|
52
|
+
self,
|
|
53
|
+
identifiers: list[str],
|
|
54
|
+
line_item: str,
|
|
55
|
+
period_type: PeriodType | None = None,
|
|
56
|
+
start_year: int | None = None,
|
|
57
|
+
end_year: int | None = None,
|
|
58
|
+
start_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
59
|
+
end_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""Sample response:
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
'SPGI': {
|
|
65
|
+
'2022': {'revenue': 11181000000.0},
|
|
66
|
+
'2023': {'revenue': 12497000000.0},
|
|
67
|
+
'2024': {'revenue': 14208000000.0}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
api_client = self.kfinance_client.kfinance_api_client
|
|
72
|
+
parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
|
|
73
|
+
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
74
|
+
identifiers=parsed_identifiers, api_client=api_client
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
tasks = [
|
|
78
|
+
Task(
|
|
79
|
+
func=api_client.fetch_line_item,
|
|
80
|
+
kwargs=dict(
|
|
81
|
+
company_id=company_id,
|
|
82
|
+
line_item=line_item,
|
|
83
|
+
period_type=period_type,
|
|
84
|
+
start_year=start_year,
|
|
85
|
+
end_year=end_year,
|
|
86
|
+
start_quarter=start_quarter,
|
|
87
|
+
end_quarter=end_quarter,
|
|
88
|
+
),
|
|
89
|
+
result_key=identifier,
|
|
90
|
+
)
|
|
91
|
+
for identifier, company_id in identifiers_to_company_ids.items()
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
line_item_responses = process_tasks_in_thread_pool_executor(
|
|
95
|
+
api_client=api_client, tasks=tasks
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
output = dict()
|
|
99
|
+
for identifier, result in line_item_responses.items():
|
|
100
|
+
df = pd.DataFrame(result).apply(pd.to_numeric).replace(np.nan, None)
|
|
101
|
+
# If no date and multiple companies, only return the most recent value.
|
|
102
|
+
# By default, we return 5 years of data, which can be too much when
|
|
103
|
+
# returning data for many companies.
|
|
104
|
+
if (
|
|
105
|
+
start_year is None
|
|
106
|
+
and end_year is None
|
|
107
|
+
and start_quarter is None
|
|
108
|
+
and end_quarter is None
|
|
109
|
+
and len(identifiers) > 1
|
|
110
|
+
):
|
|
111
|
+
df = df.tail(1)
|
|
112
|
+
output[str(identifier)] = df.transpose().set_index(pd.Index([line_item])).to_dict()
|
|
113
|
+
|
|
114
|
+
return output
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
2
|
+
from requests_mock import Mocker
|
|
3
|
+
|
|
4
|
+
from kfinance.client.kfinance import Client
|
|
5
|
+
from kfinance.conftest import SPGI_COMPANY_ID
|
|
6
|
+
from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
|
|
7
|
+
from kfinance.domains.line_items.line_item_tools import (
|
|
8
|
+
GetFinancialLineItemFromIdentifiers,
|
|
9
|
+
GetFinancialLineItemFromIdentifiersArgs,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestGetFinancialLineItemFromCompanyIds:
|
|
14
|
+
line_item_resp = {
|
|
15
|
+
"line_item": {
|
|
16
|
+
"2022": "11181000000.000000",
|
|
17
|
+
"2023": "12497000000.000000",
|
|
18
|
+
"2024": "14208000000.000000",
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def test_get_financial_line_item_from_identifiers(
|
|
23
|
+
self, mock_client: Client, requests_mock: Mocker
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
GIVEN the GetFinancialLineItemFromCompanyId tool
|
|
27
|
+
WHEN we request SPGI revenue
|
|
28
|
+
THEN we get back the SPGI revenue
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
expected_response = {
|
|
32
|
+
"SPGI": {
|
|
33
|
+
"2022": {"revenue": 11181000000.0},
|
|
34
|
+
"2023": {"revenue": 12497000000.0},
|
|
35
|
+
"2024": {"revenue": 14208000000.0},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
requests_mock.get(
|
|
40
|
+
url=f"https://kfinance.kensho.com/api/v1/line_item/{SPGI_COMPANY_ID}/revenue/none/none/none/none/none",
|
|
41
|
+
json=self.line_item_resp,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
45
|
+
args = GetFinancialLineItemFromIdentifiersArgs(identifiers=["SPGI"], line_item="revenue")
|
|
46
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
47
|
+
assert response == expected_response
|
|
48
|
+
|
|
49
|
+
def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
50
|
+
"""
|
|
51
|
+
GIVEN the GetFinancialLineItemFromIdentifiers tool
|
|
52
|
+
WHEN we request most recent line items for multiple companies
|
|
53
|
+
THEN we only get back the most recent line item for each company
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
company_ids = [1, 2]
|
|
57
|
+
expected_response = {
|
|
58
|
+
"C_1": {"2024": {"revenue": 14208000000.0}},
|
|
59
|
+
"C_2": {"2024": {"revenue": 14208000000.0}},
|
|
60
|
+
}
|
|
61
|
+
for company_id in company_ids:
|
|
62
|
+
requests_mock.get(
|
|
63
|
+
url=f"https://kfinance.kensho.com/api/v1/line_item/{company_id}/revenue/none/none/none/none/none",
|
|
64
|
+
json=self.line_item_resp,
|
|
65
|
+
)
|
|
66
|
+
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
67
|
+
args = GetFinancialLineItemFromIdentifiersArgs(
|
|
68
|
+
identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
|
|
69
|
+
line_item="revenue",
|
|
70
|
+
)
|
|
71
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
72
|
+
assert response == expected_response
|
|
73
|
+
|
|
74
|
+
def test_line_items_and_aliases_included_in_schema(self, mock_client: Client):
|
|
75
|
+
"""
|
|
76
|
+
GIVEN a GetFinancialLineItemFromCompanyIds tool
|
|
77
|
+
WHEN we generate an openai schema from the tool
|
|
78
|
+
THEN all line items and aliases are included in the line item enum
|
|
79
|
+
"""
|
|
80
|
+
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
81
|
+
oai_schema = convert_to_openai_tool(tool)
|
|
82
|
+
line_items = oai_schema["function"]["parameters"]["properties"]["line_item"]["enum"]
|
|
83
|
+
# revenue is a line item
|
|
84
|
+
assert "revenue" in line_items
|
|
85
|
+
# normal_revenue is an alias for revenue
|
|
86
|
+
assert "normal_revenue" in line_items
|
|
File without changes
|