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.
Files changed (57) hide show
  1. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/METADATA +3 -3
  2. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/RECORD +57 -56
  3. kfinance/CHANGELOG.md +51 -0
  4. kfinance/client/batch_request_handling.py +3 -1
  5. kfinance/client/fetch.py +127 -54
  6. kfinance/client/kfinance.py +38 -39
  7. kfinance/client/meta_classes.py +50 -20
  8. kfinance/client/models/date_and_period_models.py +32 -7
  9. kfinance/client/models/decimal_with_unit.py +14 -2
  10. kfinance/client/models/response_models.py +33 -0
  11. kfinance/client/models/tests/test_decimal_with_unit.py +9 -0
  12. kfinance/client/tests/test_batch_requests.py +5 -4
  13. kfinance/client/tests/test_fetch.py +134 -58
  14. kfinance/client/tests/test_objects.py +207 -145
  15. kfinance/conftest.py +10 -0
  16. kfinance/domains/business_relationships/business_relationship_tools.py +17 -8
  17. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -16
  18. kfinance/domains/capitalizations/capitalization_models.py +7 -5
  19. kfinance/domains/capitalizations/capitalization_tools.py +38 -20
  20. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +66 -36
  21. kfinance/domains/companies/company_models.py +22 -2
  22. kfinance/domains/companies/company_tools.py +49 -16
  23. kfinance/domains/companies/tests/test_company_tools.py +27 -9
  24. kfinance/domains/competitors/competitor_tools.py +19 -5
  25. kfinance/domains/competitors/tests/test_competitor_tools.py +22 -19
  26. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +29 -8
  27. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +13 -8
  28. kfinance/domains/earnings/earning_tools.py +73 -29
  29. kfinance/domains/earnings/tests/test_earnings_tools.py +52 -43
  30. kfinance/domains/line_items/line_item_models.py +372 -16
  31. kfinance/domains/line_items/line_item_tools.py +198 -46
  32. kfinance/domains/line_items/tests/test_line_item_tools.py +305 -39
  33. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +46 -2
  34. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -74
  35. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +61 -59
  36. kfinance/domains/prices/price_models.py +7 -6
  37. kfinance/domains/prices/price_tools.py +24 -16
  38. kfinance/domains/prices/tests/test_price_tools.py +47 -39
  39. kfinance/domains/segments/segment_models.py +17 -3
  40. kfinance/domains/segments/segment_tools.py +102 -42
  41. kfinance/domains/segments/tests/test_segment_tools.py +166 -37
  42. kfinance/domains/statements/statement_models.py +17 -3
  43. kfinance/domains/statements/statement_tools.py +130 -46
  44. kfinance/domains/statements/tests/test_statement_tools.py +251 -49
  45. kfinance/integrations/local_mcp/kfinance_mcp.py +1 -1
  46. kfinance/integrations/tests/test_example_notebook.py +57 -16
  47. kfinance/integrations/tool_calling/all_tools.py +5 -1
  48. kfinance/integrations/tool_calling/static_tools/get_n_quarters_ago.py +5 -0
  49. kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +13 -10
  50. kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +2 -1
  51. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +15 -4
  52. kfinance/integrations/tool_calling/tool_calling_models.py +18 -6
  53. kfinance/version.py +2 -2
  54. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/WHEEL +0 -0
  55. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  56. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/LICENSE +0 -0
  57. {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
- "line_item": {
16
- "2022": "11181000000.000000",
17
- "2023": "12497000000.000000",
18
- "2024": "14208000000.000000",
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
- "SPGI": {
33
- "2022": {"revenue": 11181000000.0},
34
- "2023": {"revenue": 12497000000.0},
35
- "2024": {"revenue": 14208000000.0},
36
- }
37
- }
38
- expected_response = {
39
- "results": {
40
- "SPGI": {
41
- "line_item": {
42
- "2022": "11181000000.000000",
43
- "2023": "12497000000.000000",
44
- "2024": "14208000000.000000",
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
- "errors": [
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
- requests_mock.get(
54
- url=f"https://kfinance.kensho.com/api/v1/line_item/{SPGI_COMPANY_ID}/revenue/none/none/none/none/none",
55
- json=self.line_item_resp,
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", "non-existent"], line_item="revenue"
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
- expected_response = {
74
- "results": {
75
- "C_1": {"line_item": {"2024": "14208000000.000000"}},
76
- "C_2": {"line_item": {"2024": "14208000000.000000"}},
77
- }
78
- }
79
- for company_id in company_ids:
80
- requests_mock.get(
81
- url=f"https://kfinance.kensho.com/api/v1/line_item/{company_id}/revenue/none/none/none/none/none",
82
- json=self.line_item_resp,
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: str
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, MergerOrAcquisition, ParticipantInMerger
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
- "Retrieves all merger and acquisition transactions involving the specified company identifier for each specified company identifier. The results are categorized by the company's role in each transaction: target, buyer, or seller. Provides the transaction_id, merger_title, and transaction closed_date (finalization) . Use this tool to answer questions like 'Which companies did Microsoft purchase?', 'Which company acquired Ben & Jerry's?', and 'Who did Pfizer acquire?'"
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]) -> dict:
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
- output_model = GetMergersFromIdentifiersResp(
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.", default=None)
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
- "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). Use this tool to answer questions like 'When was the acquisition Ben & Jerry's announced?', 'What was the transaction size of Vodafone's acquisition of Mannesmann?', 'How much did S&P purchase Kensho for?'. Always call this for announcement related questions"
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) -> dict:
101
- merger_or_acquisition = MergerOrAcquisition(
102
- kfinance_api_client=self.kfinance_client.kfinance_api_client,
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.", default=None)
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 = "get_advisors_for_company_in_transaction_from_identifier"
141
+ name: str = "get_advisors_for_company_in_transaction"
170
142
  description: str = dedent("""
171
- "Returns a list of advisor companies that provided advisory services to the specified company during a particular merger or acquisition transaction. Use this tool to answer questions like 'Who advised S&P Global during their purchase of Kensho?', 'Which firms advised Ben & Jerry's in their acquisition?'."
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(self, identifier: str, transaction_id: int) -> dict:
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
- output_model = GetAdvisorsForCompanyInTransactionFromIdentifierResp(
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=prefix_company_id(advisor.company.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
- output_model = GetAdvisorsForCompanyInTransactionFromIdentifierResp(
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")