kensho-kfinance 3.2.4__py3-none-any.whl → 4.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.
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/METADATA +3 -3
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/RECORD +57 -56
- kfinance/CHANGELOG.md +51 -0
- kfinance/client/batch_request_handling.py +3 -1
- kfinance/client/fetch.py +127 -54
- kfinance/client/kfinance.py +38 -39
- kfinance/client/meta_classes.py +50 -20
- kfinance/client/models/date_and_period_models.py +32 -7
- kfinance/client/models/decimal_with_unit.py +14 -2
- kfinance/client/models/response_models.py +33 -0
- kfinance/client/models/tests/test_decimal_with_unit.py +9 -0
- kfinance/client/tests/test_batch_requests.py +5 -4
- kfinance/client/tests/test_fetch.py +134 -58
- kfinance/client/tests/test_objects.py +207 -145
- kfinance/conftest.py +10 -0
- kfinance/domains/business_relationships/business_relationship_tools.py +17 -8
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -16
- kfinance/domains/capitalizations/capitalization_models.py +7 -5
- kfinance/domains/capitalizations/capitalization_tools.py +38 -20
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +66 -36
- kfinance/domains/companies/company_models.py +22 -2
- kfinance/domains/companies/company_tools.py +49 -16
- kfinance/domains/companies/tests/test_company_tools.py +27 -9
- kfinance/domains/competitors/competitor_tools.py +19 -5
- kfinance/domains/competitors/tests/test_competitor_tools.py +22 -19
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +29 -8
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +13 -8
- kfinance/domains/earnings/earning_tools.py +73 -29
- kfinance/domains/earnings/tests/test_earnings_tools.py +52 -43
- kfinance/domains/line_items/line_item_models.py +372 -16
- kfinance/domains/line_items/line_item_tools.py +198 -46
- kfinance/domains/line_items/tests/test_line_item_tools.py +305 -39
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +46 -2
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -74
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +61 -59
- kfinance/domains/prices/price_models.py +7 -6
- kfinance/domains/prices/price_tools.py +24 -16
- kfinance/domains/prices/tests/test_price_tools.py +47 -39
- kfinance/domains/segments/segment_models.py +17 -3
- kfinance/domains/segments/segment_tools.py +102 -42
- kfinance/domains/segments/tests/test_segment_tools.py +166 -37
- kfinance/domains/statements/statement_models.py +17 -3
- kfinance/domains/statements/statement_tools.py +130 -46
- kfinance/domains/statements/tests/test_statement_tools.py +251 -49
- kfinance/integrations/local_mcp/kfinance_mcp.py +1 -1
- kfinance/integrations/tests/test_example_notebook.py +57 -16
- kfinance/integrations/tool_calling/all_tools.py +5 -1
- kfinance/integrations/tool_calling/static_tools/get_n_quarters_ago.py +5 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +13 -10
- kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +2 -1
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +15 -4
- kfinance/integrations/tool_calling/tool_calling_models.py +18 -6
- kfinance/version.py +2 -2
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,39 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
1
3
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
2
4
|
from requests_mock import Mocker
|
|
3
5
|
|
|
4
6
|
from kfinance.client.kfinance import Client
|
|
5
|
-
from kfinance.conftest import SPGI_COMPANY_ID
|
|
6
7
|
from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
|
|
8
|
+
from kfinance.domains.line_items.line_item_models import LineItemResp, LineItemScore
|
|
7
9
|
from kfinance.domains.line_items.line_item_tools import (
|
|
8
10
|
GetFinancialLineItemFromIdentifiers,
|
|
9
11
|
GetFinancialLineItemFromIdentifiersArgs,
|
|
12
|
+
GetFinancialLineItemFromIdentifiersResp,
|
|
13
|
+
_find_similar_line_items,
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class TestGetFinancialLineItemFromCompanyIds:
|
|
14
18
|
line_item_resp = {
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
"currency": "USD",
|
|
20
|
+
"periods": {
|
|
21
|
+
"CY2022": {
|
|
22
|
+
"period_end_date": "2022-12-31",
|
|
23
|
+
"num_months": 12,
|
|
24
|
+
"line_item": {"name": "Revenue", "value": "11181000000.0", "sources": []},
|
|
25
|
+
},
|
|
26
|
+
"CY2023": {
|
|
27
|
+
"period_end_date": "2023-12-31",
|
|
28
|
+
"num_months": 12,
|
|
29
|
+
"line_item": {"name": "Revenue", "value": "12497000000.0", "sources": []},
|
|
30
|
+
},
|
|
31
|
+
"CY2024": {
|
|
32
|
+
"period_end_date": "2024-12-31",
|
|
33
|
+
"num_months": 12,
|
|
34
|
+
"line_item": {"name": "Revenue", "value": "14208000000.0", "sources": []},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
def test_get_financial_line_item_from_identifiers(
|
|
@@ -28,36 +45,72 @@ class TestGetFinancialLineItemFromCompanyIds:
|
|
|
28
45
|
THEN we get back the SPGI revenue and an error for the non-existent company
|
|
29
46
|
"""
|
|
30
47
|
|
|
31
|
-
expected_response =
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
expected_response = GetFinancialLineItemFromIdentifiersResp(
|
|
49
|
+
results={
|
|
50
|
+
"SPGI": LineItemResp(
|
|
51
|
+
currency="USD",
|
|
52
|
+
periods={
|
|
53
|
+
"CY2022": {
|
|
54
|
+
"period_end_date": "2022-12-31",
|
|
55
|
+
"num_months": 12,
|
|
56
|
+
"line_item": {
|
|
57
|
+
"name": "Revenue",
|
|
58
|
+
"value": Decimal(11181000000),
|
|
59
|
+
"sources": [],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"CY2023": {
|
|
63
|
+
"period_end_date": "2023-12-31",
|
|
64
|
+
"num_months": 12,
|
|
65
|
+
"line_item": {
|
|
66
|
+
"name": "Revenue",
|
|
67
|
+
"value": Decimal(12497000000),
|
|
68
|
+
"sources": [],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"CY2024": {
|
|
72
|
+
"period_end_date": "2024-12-31",
|
|
73
|
+
"num_months": 12,
|
|
74
|
+
"line_item": {
|
|
75
|
+
"name": "Revenue",
|
|
76
|
+
"value": Decimal(14208000000),
|
|
77
|
+
"sources": [],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
)
|
|
47
82
|
},
|
|
48
|
-
|
|
83
|
+
errors=[
|
|
49
84
|
"No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
|
|
50
85
|
],
|
|
51
|
-
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Mock the unified_fetch_id_triples response
|
|
89
|
+
requests_mock.post(
|
|
90
|
+
url="https://kfinance.kensho.com/api/v1/ids",
|
|
91
|
+
json={
|
|
92
|
+
"identifiers_to_id_triples": {
|
|
93
|
+
"SPGI": {
|
|
94
|
+
"company_id": 21719,
|
|
95
|
+
"security_id": 2629107,
|
|
96
|
+
"trading_item_id": 2629108,
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"errors": {
|
|
100
|
+
"NON-EXISTENT": "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
)
|
|
52
104
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
# Mock the fetch_line_item response
|
|
106
|
+
requests_mock.post(
|
|
107
|
+
url="https://kfinance.kensho.com/api/v1/line_item/",
|
|
108
|
+
json={"results": {"21719": self.line_item_resp}, "errors": {}},
|
|
56
109
|
)
|
|
57
110
|
|
|
58
111
|
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
59
112
|
args = GetFinancialLineItemFromIdentifiersArgs(
|
|
60
|
-
identifiers=["SPGI", "
|
|
113
|
+
identifiers=["SPGI", "NON-EXISTENT"], line_item="revenue"
|
|
61
114
|
)
|
|
62
115
|
response = tool.run(args.model_dump(mode="json"))
|
|
63
116
|
assert response == expected_response
|
|
@@ -70,17 +123,93 @@ class TestGetFinancialLineItemFromCompanyIds:
|
|
|
70
123
|
"""
|
|
71
124
|
|
|
72
125
|
company_ids = [1, 2]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
|
|
127
|
+
line_item_resp = LineItemResp(
|
|
128
|
+
currency="USD",
|
|
129
|
+
periods={
|
|
130
|
+
"CY2024": {
|
|
131
|
+
"period_end_date": "2024-12-31",
|
|
132
|
+
"num_months": 12,
|
|
133
|
+
"line_item": {"name": "Revenue", "value": Decimal(14208000000), "sources": []},
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
expected_response = GetFinancialLineItemFromIdentifiersResp(
|
|
138
|
+
results={"C_1": line_item_resp, "C_2": line_item_resp},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Mock the unified_fetch_id_triples response
|
|
142
|
+
requests_mock.post(
|
|
143
|
+
url="https://kfinance.kensho.com/api/v1/ids",
|
|
144
|
+
json={
|
|
145
|
+
"identifiers_to_id_triples": {
|
|
146
|
+
"C_1": {"company_id": 1, "security_id": 101, "trading_item_id": 201},
|
|
147
|
+
"C_2": {"company_id": 2, "security_id": 102, "trading_item_id": 202},
|
|
148
|
+
},
|
|
149
|
+
"errors": {},
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Mock the fetch_line_item response
|
|
154
|
+
requests_mock.post(
|
|
155
|
+
url="https://kfinance.kensho.com/api/v1/line_item/",
|
|
156
|
+
json={"results": {"1": self.line_item_resp, "2": self.line_item_resp}, "errors": {}},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
160
|
+
args = GetFinancialLineItemFromIdentifiersArgs(
|
|
161
|
+
identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
|
|
162
|
+
line_item="revenue",
|
|
163
|
+
)
|
|
164
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
165
|
+
assert response == expected_response
|
|
166
|
+
|
|
167
|
+
def test_empty_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
|
|
168
|
+
"""
|
|
169
|
+
GIVEN the GetFinancialLineItemFromIdentifiers tool
|
|
170
|
+
WHEN we request most recent line items for multiple companies
|
|
171
|
+
THEN we only get back the most recent line item for each company
|
|
172
|
+
UNLESS no line items exist
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
company_ids = [1, 2]
|
|
176
|
+
|
|
177
|
+
c_1_line_item_resp = LineItemResp(currency="USD", periods={})
|
|
178
|
+
c_2_line_item_resp = LineItemResp(
|
|
179
|
+
currency="USD",
|
|
180
|
+
periods={
|
|
181
|
+
"CY2024": {
|
|
182
|
+
"period_end_date": "2024-12-31",
|
|
183
|
+
"num_months": 12,
|
|
184
|
+
"line_item": {"name": "Revenue", "value": Decimal(14208000000), "sources": []},
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
expected_response = GetFinancialLineItemFromIdentifiersResp(
|
|
189
|
+
results={"C_1": c_1_line_item_resp, "C_2": c_2_line_item_resp},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Mock the unified_fetch_id_triples response
|
|
193
|
+
requests_mock.post(
|
|
194
|
+
url="https://kfinance.kensho.com/api/v1/ids",
|
|
195
|
+
json={
|
|
196
|
+
"identifiers_to_id_triples": {
|
|
197
|
+
"C_1": {"company_id": 1, "security_id": 101, "trading_item_id": 201},
|
|
198
|
+
"C_2": {"company_id": 2, "security_id": 102, "trading_item_id": 202},
|
|
199
|
+
},
|
|
200
|
+
"errors": {},
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Mock the fetch_line_item response with different data for different companies
|
|
205
|
+
requests_mock.post(
|
|
206
|
+
url="https://kfinance.kensho.com/api/v1/line_item/",
|
|
207
|
+
json={
|
|
208
|
+
"results": {"1": {"currency": "USD", "periods": {}}, "2": self.line_item_resp},
|
|
209
|
+
"errors": {},
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
84
213
|
tool = GetFinancialLineItemFromIdentifiers(kfinance_client=mock_client)
|
|
85
214
|
args = GetFinancialLineItemFromIdentifiersArgs(
|
|
86
215
|
identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
|
|
@@ -102,3 +231,140 @@ class TestGetFinancialLineItemFromCompanyIds:
|
|
|
102
231
|
assert "revenue" in line_items
|
|
103
232
|
# normal_revenue is an alias for revenue
|
|
104
233
|
assert "normal_revenue" in line_items
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class TestFindSimilarLineItems:
|
|
237
|
+
"""Tests for the _find_similar_line_items function."""
|
|
238
|
+
|
|
239
|
+
# Preset test descriptors to ensure consistent results
|
|
240
|
+
TEST_DESCRIPTORS = {
|
|
241
|
+
"revenue": "Revenue recognized from primary business activities (excludes non-operating income).",
|
|
242
|
+
"total_revenue": "Sum of operating and non-operating revenue streams for the period.",
|
|
243
|
+
"cost_of_goods_sold": "Direct costs attributable to producing goods sold during the period.",
|
|
244
|
+
"cogs": "Direct costs attributable to producing goods sold during the period.",
|
|
245
|
+
"gross_profit": "Revenue minus cost_of_goods_sold or cost_of_revenue for the reported period.",
|
|
246
|
+
"operating_income": "Operating profit after subtracting operating expenses from operating revenue.",
|
|
247
|
+
"net_income": "Bottom-line profit attributable to common shareholders.",
|
|
248
|
+
"research_and_development_expense": "Expenses incurred for research and development activities.",
|
|
249
|
+
"r_and_d_expense": "Expenses incurred for research and development activities.",
|
|
250
|
+
"depreciation_and_amortization": "Combined depreciation and amortization expense for the period.",
|
|
251
|
+
"ebitda": "Earnings before interest, taxes, depreciation, and amortization.",
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
def test_exact_keyword_match(self):
|
|
255
|
+
"""
|
|
256
|
+
GIVEN a preset descriptors dictionary
|
|
257
|
+
WHEN searching for 'revenues' (similar to 'revenue')
|
|
258
|
+
THEN 'revenue' should be in the top suggestions
|
|
259
|
+
"""
|
|
260
|
+
results = _find_similar_line_items("revenues", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
261
|
+
|
|
262
|
+
assert len(results) > 0
|
|
263
|
+
assert isinstance(results[0], LineItemScore)
|
|
264
|
+
# Check that revenue or total_revenue is in top results
|
|
265
|
+
result_names = [item.name for item in results]
|
|
266
|
+
assert "revenue" in result_names or "total_revenue" in result_names
|
|
267
|
+
|
|
268
|
+
def test_acronym_matching(self):
|
|
269
|
+
"""
|
|
270
|
+
GIVEN a preset descriptors dictionary
|
|
271
|
+
WHEN searching for 'R&D' (abbreviation)
|
|
272
|
+
THEN research and development related items should appear
|
|
273
|
+
"""
|
|
274
|
+
results = _find_similar_line_items("R&D", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
275
|
+
|
|
276
|
+
result_names = [item.name for item in results]
|
|
277
|
+
# Should find r_and_d_expense or research_and_development_expense
|
|
278
|
+
assert any("research" in name or "r_and_d" in name for name in result_names)
|
|
279
|
+
|
|
280
|
+
def test_multiple_word_matching(self):
|
|
281
|
+
"""
|
|
282
|
+
GIVEN a preset descriptors dictionary
|
|
283
|
+
WHEN searching for 'cost goods'
|
|
284
|
+
THEN 'cost_of_goods_sold' should be suggested
|
|
285
|
+
"""
|
|
286
|
+
results = _find_similar_line_items("cost goods", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
287
|
+
|
|
288
|
+
result_names = [item.name for item in results]
|
|
289
|
+
assert "cost_of_goods_sold" in result_names or "cogs" in result_names
|
|
290
|
+
|
|
291
|
+
def test_description_matching(self):
|
|
292
|
+
"""
|
|
293
|
+
GIVEN a preset descriptors dictionary
|
|
294
|
+
WHEN searching for 'profit'
|
|
295
|
+
THEN items with 'profit' in description should appear
|
|
296
|
+
"""
|
|
297
|
+
results = _find_similar_line_items("profit", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
298
|
+
|
|
299
|
+
assert len(results) > 0
|
|
300
|
+
# Should find items like gross_profit, operating_income (operating profit), or net_income
|
|
301
|
+
result_names = [item.name for item in results]
|
|
302
|
+
assert any("profit" in name or "income" in name for name in result_names)
|
|
303
|
+
|
|
304
|
+
def test_empty_descriptors(self):
|
|
305
|
+
"""
|
|
306
|
+
GIVEN an empty descriptors dictionary
|
|
307
|
+
WHEN searching for any term
|
|
308
|
+
THEN should return empty list
|
|
309
|
+
"""
|
|
310
|
+
results = _find_similar_line_items("revenue", {}, max_suggestions=5)
|
|
311
|
+
assert results == []
|
|
312
|
+
|
|
313
|
+
def test_no_matches(self):
|
|
314
|
+
"""
|
|
315
|
+
GIVEN a preset descriptors dictionary
|
|
316
|
+
WHEN searching for completely unrelated term
|
|
317
|
+
THEN should return empty list or very low scores filtered out
|
|
318
|
+
"""
|
|
319
|
+
results = _find_similar_line_items("xyz123abc", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
320
|
+
# Should return empty or very few results since threshold is > 0.1
|
|
321
|
+
assert len(results) <= 2 # May have some weak matches but should be minimal
|
|
322
|
+
|
|
323
|
+
def test_max_suggestions_respected(self):
|
|
324
|
+
"""
|
|
325
|
+
GIVEN a preset descriptors dictionary
|
|
326
|
+
WHEN searching with max_suggestions=3
|
|
327
|
+
THEN should return at most 3 results
|
|
328
|
+
"""
|
|
329
|
+
results = _find_similar_line_items("income", self.TEST_DESCRIPTORS, max_suggestions=3)
|
|
330
|
+
assert len(results) <= 3
|
|
331
|
+
|
|
332
|
+
def test_score_ordering(self):
|
|
333
|
+
"""
|
|
334
|
+
GIVEN a preset descriptors dictionary
|
|
335
|
+
WHEN searching for a term
|
|
336
|
+
THEN results should be ordered by descending score
|
|
337
|
+
"""
|
|
338
|
+
results = _find_similar_line_items("revenue", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
339
|
+
|
|
340
|
+
if len(results) > 1:
|
|
341
|
+
for i in range(len(results) - 1):
|
|
342
|
+
assert results[i].score >= results[i + 1].score
|
|
343
|
+
|
|
344
|
+
def test_score_threshold(self):
|
|
345
|
+
"""
|
|
346
|
+
GIVEN a preset descriptors dictionary
|
|
347
|
+
WHEN searching for a term
|
|
348
|
+
THEN all returned results should have score > 0.1
|
|
349
|
+
"""
|
|
350
|
+
results = _find_similar_line_items("revenue", self.TEST_DESCRIPTORS, max_suggestions=10)
|
|
351
|
+
|
|
352
|
+
for item in results:
|
|
353
|
+
assert item.score > 0.1
|
|
354
|
+
|
|
355
|
+
def test_lineitemscore_structure(self):
|
|
356
|
+
"""
|
|
357
|
+
GIVEN a preset descriptors dictionary
|
|
358
|
+
WHEN searching for a term
|
|
359
|
+
THEN each result should be a LineItemScore with name, description, and score
|
|
360
|
+
"""
|
|
361
|
+
results = _find_similar_line_items("revenue", self.TEST_DESCRIPTORS, max_suggestions=5)
|
|
362
|
+
|
|
363
|
+
assert len(results) > 0
|
|
364
|
+
for item in results:
|
|
365
|
+
assert isinstance(item, LineItemScore)
|
|
366
|
+
assert isinstance(item.name, str)
|
|
367
|
+
assert isinstance(item.description, str)
|
|
368
|
+
assert isinstance(item.score, float)
|
|
369
|
+
assert item.name in self.TEST_DESCRIPTORS
|
|
370
|
+
assert item.description == self.TEST_DESCRIPTORS[item.name]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
2
3
|
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
+
from pydantic import BaseModel, field_serializer
|
|
5
|
+
|
|
6
|
+
from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX, CompanyIdAndName
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class MergerSummary(BaseModel):
|
|
@@ -16,6 +19,47 @@ class MergersResp(BaseModel):
|
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class AdvisorResp(BaseModel):
|
|
19
|
-
advisor_company_id:
|
|
22
|
+
advisor_company_id: int
|
|
20
23
|
advisor_company_name: str
|
|
21
24
|
advisor_type_name: str | None
|
|
25
|
+
|
|
26
|
+
@field_serializer("advisor_company_id")
|
|
27
|
+
def serialize_with_prefix(self, company_id: int) -> str:
|
|
28
|
+
"""Serialize the advisor_company_id with a prefix ("C_<company_id>").
|
|
29
|
+
|
|
30
|
+
Including the prefix allows us to distinguish tickers and company_ids.
|
|
31
|
+
"""
|
|
32
|
+
return f"{COMPANY_ID_PREFIX}{company_id}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MergerTimelineElement(BaseModel):
|
|
36
|
+
status: str
|
|
37
|
+
date: date
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MergerParticipants(BaseModel):
|
|
41
|
+
target: CompanyIdAndName
|
|
42
|
+
buyers: list[CompanyIdAndName]
|
|
43
|
+
sellers: list[CompanyIdAndName]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MergerConsiderationDetail(BaseModel):
|
|
47
|
+
scenario: str | None = None
|
|
48
|
+
subtype: str | None = None
|
|
49
|
+
cash_or_cash_equivalent_per_target_share_unit: Decimal | None = None
|
|
50
|
+
number_of_target_shares_sought: Decimal | None = None
|
|
51
|
+
current_calculated_gross_value_of_consideration: Decimal | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MergerConsideration(BaseModel):
|
|
55
|
+
currency_name: str | None = None
|
|
56
|
+
current_calculated_gross_total_transaction_value: Decimal | None = None
|
|
57
|
+
current_calculated_implied_equity_value: Decimal | None = None
|
|
58
|
+
current_calculated_implied_enterprise_value: Decimal | None = None
|
|
59
|
+
details: list[MergerConsiderationDetail]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MergerInfo(BaseModel):
|
|
63
|
+
timeline: list[MergerTimelineElement]
|
|
64
|
+
participants: MergerParticipants
|
|
65
|
+
consideration: MergerConsideration
|
|
@@ -4,11 +4,11 @@ from typing import Type
|
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
7
|
-
from kfinance.client.kfinance import Company,
|
|
7
|
+
from kfinance.client.kfinance import Company, ParticipantInMerger
|
|
8
8
|
from kfinance.client.permission_models import Permission
|
|
9
|
-
from kfinance.domains.companies.company_models import prefix_company_id
|
|
10
9
|
from kfinance.domains.mergers_and_acquisitions.merger_and_acquisition_models import (
|
|
11
10
|
AdvisorResp,
|
|
11
|
+
MergerInfo,
|
|
12
12
|
MergersResp,
|
|
13
13
|
)
|
|
14
14
|
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
@@ -26,12 +26,24 @@ class GetMergersFromIdentifiersResp(ToolRespWithErrors):
|
|
|
26
26
|
class GetMergersFromIdentifiers(KfinanceTool):
|
|
27
27
|
name: str = "get_mergers_from_identifiers"
|
|
28
28
|
description: str = dedent("""
|
|
29
|
-
|
|
29
|
+
Retrieves all merger and acquisition transactions involving the specified company.
|
|
30
|
+
|
|
31
|
+
Results are categorized by the company's role: target (being acquired), buyer (making the acquisition), or seller (divesting an asset).
|
|
32
|
+
|
|
33
|
+
- When possible, pass multiple identifiers in a single call rather than making multiple calls.
|
|
34
|
+
- Provides transaction_id, merger_title, and transaction closed_date.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
Query: "Which companies did Microsoft purchase?"
|
|
38
|
+
Function: get_mergers_from_identifiers(identifiers=["Microsoft"])
|
|
39
|
+
|
|
40
|
+
Query: "Get acquisitions for AAPL and GOOGL"
|
|
41
|
+
Function: get_mergers_from_identifiers(identifiers=["AAPL", "GOOGL"])
|
|
30
42
|
""").strip()
|
|
31
43
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
|
|
32
44
|
accepted_permissions: set[Permission] | None = {Permission.MergersPermission}
|
|
33
45
|
|
|
34
|
-
def _run(self, identifiers: list[str]) ->
|
|
46
|
+
def _run(self, identifiers: list[str]) -> GetMergersFromIdentifiersResp:
|
|
35
47
|
"""Sample Response:
|
|
36
48
|
|
|
37
49
|
{
|
|
@@ -79,86 +91,46 @@ class GetMergersFromIdentifiers(KfinanceTool):
|
|
|
79
91
|
merger_responses: dict[str, MergersResp] = process_tasks_in_thread_pool_executor(
|
|
80
92
|
api_client=api_client, tasks=tasks
|
|
81
93
|
)
|
|
82
|
-
|
|
94
|
+
return GetMergersFromIdentifiersResp(
|
|
83
95
|
results=merger_responses, errors=list(id_triple_resp.errors.values())
|
|
84
96
|
)
|
|
85
|
-
return output_model.model_dump(mode="json")
|
|
86
97
|
|
|
87
98
|
|
|
88
99
|
class GetMergerInfoFromTransactionIdArgs(BaseModel):
|
|
89
|
-
transaction_id: int | None = Field(description="The ID of the transaction."
|
|
100
|
+
transaction_id: int | None = Field(description="The ID of the transaction.")
|
|
90
101
|
|
|
91
102
|
|
|
92
103
|
class GetMergerInfoFromTransactionId(KfinanceTool):
|
|
93
104
|
name: str = "get_merger_info_from_transaction_id"
|
|
94
105
|
description: str = dedent("""
|
|
95
|
-
|
|
106
|
+
Provides comprehensive information about a specific merger or acquisition transaction, including its timeline (announced date, closed date), participants' company_name and company_id (target, buyers, sellers), and financial consideration details (including monetary values).
|
|
107
|
+
|
|
108
|
+
Use this tool for questions about announcement dates and transaction details.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
Query: "When was the acquisition of Ben & Jerry's announced?"
|
|
112
|
+
Function 1: get_mergers_from_identifiers(identifiers=["Ben & Jerry's"])
|
|
113
|
+
# Function 1 returns all M&A's that involved Ben & Jerry's. Extract the <key_dev_id> from the response where Ben & Jerry's was the target.
|
|
114
|
+
Function 2: get_merger_info_from_transaction_id(transaction_id=<key_dev_id>)
|
|
115
|
+
|
|
116
|
+
Query: "What was the transaction size of Vodafone's acquisition of Mannesmann?"
|
|
117
|
+
Function 1: get_mergers_from_identifiers(identifiers=["Vodafone"])
|
|
118
|
+
# Function 1 returns all M&A's that involved Vodafone. Extract the <key_dev_id> from the response where Vodafone was the buyer and Mannesmann was the target.
|
|
119
|
+
Function 2: get_merger_info_from_transaction_id(transaction_id=<key_dev_id>)
|
|
120
|
+
|
|
121
|
+
|
|
96
122
|
""").strip()
|
|
97
123
|
args_schema: Type[BaseModel] = GetMergerInfoFromTransactionIdArgs
|
|
98
124
|
accepted_permissions: set[Permission] | None = {Permission.MergersPermission}
|
|
99
125
|
|
|
100
|
-
def _run(self, transaction_id: int) ->
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
transaction_id=transaction_id,
|
|
104
|
-
merger_title=None,
|
|
105
|
-
closed_date=None,
|
|
126
|
+
def _run(self, transaction_id: int) -> MergerInfo:
|
|
127
|
+
return self.kfinance_client.kfinance_api_client.fetch_merger_info(
|
|
128
|
+
transaction_id=transaction_id
|
|
106
129
|
)
|
|
107
|
-
merger_timeline = merger_or_acquisition.get_timeline
|
|
108
|
-
merger_participants = merger_or_acquisition.get_participants
|
|
109
|
-
merger_consideration = merger_or_acquisition.get_consideration
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
"timeline": [
|
|
113
|
-
{"status": timeline["status"], "date": timeline["date"].strftime("%Y-%m-%d")}
|
|
114
|
-
for timeline in merger_timeline.to_dict(orient="records")
|
|
115
|
-
]
|
|
116
|
-
if merger_timeline is not None
|
|
117
|
-
else None,
|
|
118
|
-
"participants": {
|
|
119
|
-
"target": {
|
|
120
|
-
"company_id": prefix_company_id(
|
|
121
|
-
merger_participants["target"].company.company_id
|
|
122
|
-
),
|
|
123
|
-
"company_name": merger_participants["target"].company.name,
|
|
124
|
-
},
|
|
125
|
-
"buyers": [
|
|
126
|
-
{
|
|
127
|
-
"company_id": prefix_company_id(buyer.company.company_id),
|
|
128
|
-
"company_name": buyer.company.name,
|
|
129
|
-
}
|
|
130
|
-
for buyer in merger_participants["buyers"]
|
|
131
|
-
],
|
|
132
|
-
"sellers": [
|
|
133
|
-
{
|
|
134
|
-
"company_id": prefix_company_id(seller.company.company_id),
|
|
135
|
-
"company_name": seller.company.name,
|
|
136
|
-
}
|
|
137
|
-
for seller in merger_participants["sellers"]
|
|
138
|
-
],
|
|
139
|
-
}
|
|
140
|
-
if merger_participants is not None
|
|
141
|
-
else None,
|
|
142
|
-
"consideration": {
|
|
143
|
-
"currency_name": merger_consideration["currency_name"],
|
|
144
|
-
"current_calculated_gross_total_transaction_value": merger_consideration[
|
|
145
|
-
"current_calculated_gross_total_transaction_value"
|
|
146
|
-
],
|
|
147
|
-
"current_calculated_implied_equity_value": merger_consideration[
|
|
148
|
-
"current_calculated_implied_equity_value"
|
|
149
|
-
],
|
|
150
|
-
"current_calculated_implied_enterprise_value": merger_consideration[
|
|
151
|
-
"current_calculated_implied_enterprise_value"
|
|
152
|
-
],
|
|
153
|
-
"details": merger_consideration["details"].to_dict(orient="records"),
|
|
154
|
-
}
|
|
155
|
-
if merger_consideration is not None
|
|
156
|
-
else None,
|
|
157
|
-
}
|
|
158
130
|
|
|
159
131
|
|
|
160
132
|
class GetAdvisorsForCompanyInTransactionFromIdentifierArgs(ToolArgsWithIdentifier):
|
|
161
|
-
transaction_id: int | None = Field(description="The ID of the merger."
|
|
133
|
+
transaction_id: int | None = Field(description="The ID of the merger.")
|
|
162
134
|
|
|
163
135
|
|
|
164
136
|
class GetAdvisorsForCompanyInTransactionFromIdentifierResp(ToolRespWithErrors):
|
|
@@ -166,22 +138,32 @@ class GetAdvisorsForCompanyInTransactionFromIdentifierResp(ToolRespWithErrors):
|
|
|
166
138
|
|
|
167
139
|
|
|
168
140
|
class GetAdvisorsForCompanyInTransactionFromIdentifier(KfinanceTool):
|
|
169
|
-
name: str = "
|
|
141
|
+
name: str = "get_advisors_for_company_in_transaction"
|
|
170
142
|
description: str = dedent("""
|
|
171
|
-
|
|
143
|
+
Returns a list of advisor companies that provided advisory services to the specified company during a particular merger or acquisition transaction.
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
Query: "Who advised S&P Global during their purchase of Kensho?"
|
|
147
|
+
Function 1: get_mergers_from_identifiers(identifiers=["S&P Global"])
|
|
148
|
+
# Function 1 returns all M&A's that involved S&P Global. Extract the <key_dev_id> from the response where S&P Global was the buyer and Kensho was the target.
|
|
149
|
+
Function 2: get_advisors_for_company_in_transaction(identifier="S&P Global", transaction_id=<key_dev_id>)
|
|
150
|
+
|
|
151
|
+
Query: "Which firms advised AAPL in transaction 67890?"
|
|
152
|
+
Function: get_advisors_for_company_in_transaction(identifier="AAPL", transaction_id=67890)
|
|
172
153
|
""").strip()
|
|
173
154
|
args_schema: Type[BaseModel] = GetAdvisorsForCompanyInTransactionFromIdentifierArgs
|
|
174
155
|
accepted_permissions: set[Permission] | None = {Permission.MergersPermission}
|
|
175
156
|
|
|
176
|
-
def _run(
|
|
157
|
+
def _run(
|
|
158
|
+
self, identifier: str, transaction_id: int
|
|
159
|
+
) -> GetAdvisorsForCompanyInTransactionFromIdentifierResp:
|
|
177
160
|
api_client = self.kfinance_client.kfinance_api_client
|
|
178
161
|
id_triple_resp = api_client.unified_fetch_id_triples(identifiers=[identifier])
|
|
179
162
|
# If the identifier cannot be resolved, return the associated error.
|
|
180
163
|
if id_triple_resp.errors:
|
|
181
|
-
|
|
164
|
+
return GetAdvisorsForCompanyInTransactionFromIdentifierResp(
|
|
182
165
|
results=[], errors=list(id_triple_resp.errors.values())
|
|
183
166
|
)
|
|
184
|
-
return output_model.model_dump(mode="json")
|
|
185
167
|
|
|
186
168
|
id_triple = id_triple_resp.identifiers_to_id_triples[identifier]
|
|
187
169
|
|
|
@@ -201,13 +183,12 @@ class GetAdvisorsForCompanyInTransactionFromIdentifier(KfinanceTool):
|
|
|
201
183
|
for advisor in advisors:
|
|
202
184
|
advisors_response.append(
|
|
203
185
|
AdvisorResp(
|
|
204
|
-
advisor_company_id=
|
|
186
|
+
advisor_company_id=advisor.company.company_id,
|
|
205
187
|
advisor_company_name=advisor.company.name,
|
|
206
188
|
advisor_type_name=advisor.advisor_type_name,
|
|
207
189
|
)
|
|
208
190
|
)
|
|
209
191
|
|
|
210
|
-
|
|
192
|
+
return GetAdvisorsForCompanyInTransactionFromIdentifierResp(
|
|
211
193
|
results=advisors_response, errors=list(id_triple_resp.errors.values())
|
|
212
194
|
)
|
|
213
|
-
return output_model.model_dump(mode="json")
|