kensho-kfinance 3.0.3__py3-none-any.whl → 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kensho-kfinance might be problematic. Click here for more details.

Files changed (46) hide show
  1. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/METADATA +1 -1
  2. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/RECORD +46 -45
  3. kfinance/CHANGELOG.md +6 -0
  4. kfinance/client/fetch.py +26 -17
  5. kfinance/client/kfinance.py +17 -18
  6. kfinance/client/meta_classes.py +2 -2
  7. kfinance/client/tests/test_fetch.py +36 -24
  8. kfinance/client/tests/test_objects.py +112 -120
  9. kfinance/conftest.py +49 -5
  10. kfinance/domains/business_relationships/business_relationship_tools.py +30 -19
  11. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -15
  12. kfinance/domains/capitalizations/capitalization_models.py +1 -1
  13. kfinance/domains/capitalizations/capitalization_tools.py +41 -24
  14. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +38 -13
  15. kfinance/domains/companies/company_identifiers.py +0 -175
  16. kfinance/domains/companies/company_models.py +98 -5
  17. kfinance/domains/companies/company_tools.py +33 -29
  18. kfinance/domains/companies/tests/test_company_tools.py +11 -4
  19. kfinance/domains/competitors/competitor_tools.py +21 -21
  20. kfinance/domains/competitors/tests/test_competitor_tools.py +21 -7
  21. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +38 -26
  22. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +27 -26
  23. kfinance/domains/earnings/earning_tools.py +54 -47
  24. kfinance/domains/earnings/tests/test_earnings_tools.py +58 -63
  25. kfinance/domains/line_items/line_item_tools.py +29 -36
  26. kfinance/domains/line_items/tests/test_line_item_tools.py +23 -5
  27. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +15 -0
  28. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -38
  29. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +22 -9
  30. kfinance/domains/prices/price_models.py +9 -9
  31. kfinance/domains/prices/price_tools.py +49 -38
  32. kfinance/domains/prices/tests/test_price_tools.py +52 -36
  33. kfinance/domains/segments/segment_models.py +7 -0
  34. kfinance/domains/segments/segment_tools.py +37 -20
  35. kfinance/domains/segments/tests/test_segment_tools.py +13 -6
  36. kfinance/domains/statements/statement_models.py +7 -0
  37. kfinance/domains/statements/statement_tools.py +38 -40
  38. kfinance/domains/statements/tests/test_statement_tools.py +39 -10
  39. kfinance/integrations/tool_calling/prompts.py +21 -14
  40. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +2 -2
  41. kfinance/integrations/tool_calling/tool_calling_models.py +24 -5
  42. kfinance/version.py +16 -3
  43. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/WHEEL +0 -0
  44. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/licenses/AUTHORS.md +0 -0
  45. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/licenses/LICENSE +0 -0
  46. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/top_level.txt +0 -0
@@ -17,8 +17,8 @@ class TestGetHistoryMetadataFromIdentifiers:
17
17
  ):
18
18
  """
19
19
  GIVEN the GetHistoryMetadataFromIdentifiers tool
20
- WHEN we request the history metadata for SPGI
21
- THEN we get back SPGI's history metadata
20
+ WHEN we request the history metadata for SPGI and a non-existent company
21
+ THEN we get back SPGI's history metadata and an error for the non-existent company.
22
22
  """
23
23
  metadata_resp = {
24
24
  "currency": "USD",
@@ -27,7 +27,12 @@ class TestGetHistoryMetadataFromIdentifiers:
27
27
  "instrument_type": "Equity",
28
28
  "symbol": "SPGI",
29
29
  }
30
- expected_resp = {"SPGI": metadata_resp}
30
+ expected_resp = {
31
+ "results": {"SPGI": metadata_resp},
32
+ "errors": [
33
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
34
+ ],
35
+ }
31
36
 
32
37
  requests_mock.get(
33
38
  url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/metadata",
@@ -35,11 +40,13 @@ class TestGetHistoryMetadataFromIdentifiers:
35
40
  )
36
41
 
37
42
  tool = GetHistoryMetadataFromIdentifiers(kfinance_client=mock_client)
38
- resp = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
43
+ resp = tool.run(
44
+ ToolArgsWithIdentifiers(identifiers=["SPGI", "non-existent"]).model_dump(mode="json")
45
+ )
39
46
  assert resp == expected_resp
40
47
 
41
48
 
42
- class TestPricesFromIdentifiers:
49
+ class TestGetPricesFromIdentifiers:
43
50
  prices_resp = {
44
51
  "currency": "USD",
45
52
  "prices": [
@@ -65,8 +72,8 @@ class TestPricesFromIdentifiers:
65
72
  def test_get_prices_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
66
73
  """
67
74
  GIVEN the GetPricesFromIdentifiers tool
68
- WHEN we request prices for SPGI
69
- THEN we get back prices for SPGI
75
+ WHEN we request prices for SPGI and a non-existent company
76
+ THEN we get back prices for SPGI and an erro for the non-existent company
70
77
  """
71
78
 
72
79
  requests_mock.get(
@@ -74,31 +81,38 @@ class TestPricesFromIdentifiers:
74
81
  json=self.prices_resp,
75
82
  )
76
83
  expected_response = {
77
- "SPGI": {
78
- "prices": [
79
- {
80
- "date": "2024-04-11",
81
- "open": {"value": "424.26", "unit": "USD"},
82
- "high": {"value": "425.99", "unit": "USD"},
83
- "low": {"value": "422.04", "unit": "USD"},
84
- "close": {"value": "422.92", "unit": "USD"},
85
- "volume": {"value": "1129158", "unit": "Shares"},
86
- },
87
- {
88
- "date": "2024-04-12",
89
- "open": {"value": "419.23", "unit": "USD"},
90
- "high": {"value": "421.94", "unit": "USD"},
91
- "low": {"value": "416.45", "unit": "USD"},
92
- "close": {"value": "417.81", "unit": "USD"},
93
- "volume": {"value": "1182229", "unit": "Shares"},
94
- },
95
- ]
96
- }
84
+ "results": {
85
+ "SPGI": {
86
+ "prices": [
87
+ {
88
+ "date": "2024-04-11",
89
+ "open": {"value": "424.26", "unit": "USD"},
90
+ "high": {"value": "425.99", "unit": "USD"},
91
+ "low": {"value": "422.04", "unit": "USD"},
92
+ "close": {"value": "422.92", "unit": "USD"},
93
+ "volume": {"value": "1129158", "unit": "Shares"},
94
+ },
95
+ {
96
+ "date": "2024-04-12",
97
+ "open": {"value": "419.23", "unit": "USD"},
98
+ "high": {"value": "421.94", "unit": "USD"},
99
+ "low": {"value": "416.45", "unit": "USD"},
100
+ "close": {"value": "417.81", "unit": "USD"},
101
+ "volume": {"value": "1182229", "unit": "Shares"},
102
+ },
103
+ ]
104
+ }
105
+ },
106
+ "errors": [
107
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
108
+ ],
97
109
  }
98
110
 
99
111
  tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
100
112
  response = tool.run(
101
- GetPricesFromIdentifiersArgs(identifiers=["SPGI"]).model_dump(mode="json")
113
+ GetPricesFromIdentifiersArgs(identifiers=["SPGI", "non-existent"]).model_dump(
114
+ mode="json"
115
+ )
102
116
  )
103
117
  assert response == expected_response
104
118
 
@@ -119,18 +133,20 @@ class TestPricesFromIdentifiers:
119
133
  expected_single_company_response = {
120
134
  "prices": [
121
135
  {
122
- "date": "2024-04-11",
123
- "open": {"value": "424.26", "unit": "USD"},
124
- "high": {"value": "425.99", "unit": "USD"},
125
- "low": {"value": "422.04", "unit": "USD"},
126
- "close": {"value": "422.92", "unit": "USD"},
127
- "volume": {"value": "1129158", "unit": "Shares"},
136
+ "date": "2024-04-12",
137
+ "open": {"value": "419.23", "unit": "USD"},
138
+ "high": {"value": "421.94", "unit": "USD"},
139
+ "low": {"value": "416.45", "unit": "USD"},
140
+ "close": {"value": "417.81", "unit": "USD"},
141
+ "volume": {"value": "1182229", "unit": "Shares"},
128
142
  }
129
143
  ]
130
144
  }
131
145
  expected_response = {
132
- "C_1": expected_single_company_response,
133
- "C_2": expected_single_company_response,
146
+ "results": {
147
+ "C_1": expected_single_company_response,
148
+ "C_2": expected_single_company_response,
149
+ },
134
150
  }
135
151
  tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
136
152
  response = tool.run(
@@ -1,3 +1,6 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
1
4
  from strenum import StrEnum
2
5
 
3
6
 
@@ -6,3 +9,7 @@ class SegmentType(StrEnum):
6
9
 
7
10
  business = "business"
8
11
  geographic = "geographic"
12
+
13
+
14
+ class SegmentsResp(BaseModel):
15
+ segments: dict[str, Any]
@@ -5,14 +5,11 @@ from pydantic import BaseModel, Field
5
5
  from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
6
6
  from kfinance.client.models.date_and_period_models import PeriodType
7
7
  from kfinance.client.permission_models import Permission
8
- from kfinance.domains.companies.company_identifiers import (
9
- fetch_company_ids_from_identifiers,
10
- parse_identifiers,
11
- )
12
- from kfinance.domains.segments.segment_models import SegmentType
8
+ from kfinance.domains.segments.segment_models import SegmentsResp, SegmentType
13
9
  from kfinance.integrations.tool_calling.tool_calling_models import (
14
10
  KfinanceTool,
15
11
  ToolArgsWithIdentifiers,
12
+ ToolRespWithErrors,
16
13
  ValidQuarter,
17
14
  )
18
15
 
@@ -27,6 +24,10 @@ class GetSegmentsFromIdentifiersArgs(ToolArgsWithIdentifiers):
27
24
  end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
28
25
 
29
26
 
27
+ class GetSegmentsFromIdentifiersResp(ToolRespWithErrors):
28
+ results: dict[str, SegmentsResp]
29
+
30
+
30
31
  class GetSegmentsFromIdentifiers(KfinanceTool):
31
32
  name: str = "get_segments_from_identifiers"
32
33
  description: str = "Get the templated segments associated with a list of identifiers."
@@ -43,17 +44,33 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
43
44
  start_quarter: Literal[1, 2, 3, 4] | None = None,
44
45
  end_quarter: Literal[1, 2, 3, 4] | None = None,
45
46
  ) -> dict:
47
+ """Sample Response:
48
+
49
+ {
50
+ 'results': {
51
+ 'SPGI': {
52
+ 'segments': {
53
+ '2021': {
54
+ 'Commodity Insights': {'CAPEX': -2000000.0, 'D&A': 12000000.0},
55
+ 'Unallocated Assets Held for Sale': {'Total Assets': 321000000.0}
56
+ }
57
+ }
58
+ }
59
+ },
60
+ 'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
61
+ }
62
+
63
+
64
+ """
65
+
46
66
  api_client = self.kfinance_client.kfinance_api_client
47
- parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
48
- identifiers_to_company_ids = fetch_company_ids_from_identifiers(
49
- identifiers=parsed_identifiers, api_client=api_client
50
- )
67
+ id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
51
68
 
52
69
  tasks = [
53
70
  Task(
54
71
  func=api_client.fetch_segments,
55
72
  kwargs=dict(
56
- company_id=company_id,
73
+ company_id=id_triple.company_id,
57
74
  segment_type=segment_type,
58
75
  period_type=period_type,
59
76
  start_year=start_year,
@@ -63,10 +80,10 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
63
80
  ),
64
81
  result_key=identifier,
65
82
  )
66
- for identifier, company_id in identifiers_to_company_ids.items()
83
+ for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
67
84
  ]
68
85
 
69
- segments_responses = process_tasks_in_thread_pool_executor(
86
+ segments_responses: dict[str, SegmentsResp] = process_tasks_in_thread_pool_executor(
70
87
  api_client=api_client, tasks=tasks
71
88
  )
72
89
 
@@ -78,14 +95,14 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
78
95
  and end_year is None
79
96
  and start_quarter is None
80
97
  and end_quarter is None
81
- and len(identifiers) > 1
98
+ and len(segments_responses) > 1
82
99
  ):
83
100
  for segments_response in segments_responses.values():
84
- most_recent_year = max(segments_response["segments"].keys())
85
- most_recent_year_data = segments_response["segments"][most_recent_year]
86
- segments_response["segments"] = {most_recent_year: most_recent_year_data}
101
+ most_recent_year = max(segments_response.segments.keys())
102
+ most_recent_year_data = segments_response.segments[most_recent_year]
103
+ segments_response.segments = {most_recent_year: most_recent_year_data}
87
104
 
88
- return {
89
- str(identifier): segments["segments"]
90
- for identifier, segments in segments_responses.items()
91
- }
105
+ output_model = GetSegmentsFromIdentifiersResp(
106
+ results=segments_responses, errors=list(id_triple_resp.errors.values())
107
+ )
108
+ return output_model.model_dump(mode="json")
@@ -33,8 +33,8 @@ class TestGetSegmentsFromIdentifier:
33
33
  def test_get_segments_from_identifier(self, mock_client: Client, requests_mock: Mocker):
34
34
  """
35
35
  GIVEN the GetSegmentsFromIdentifier tool
36
- WHEN we request the SPGI business segment
37
- THEN we get back the SPGI business segment
36
+ WHEN we request the business segment for SPGI and an non-existent company
37
+ THEN we get back the SPGI business segment and an error for the non-existent company.
38
38
  """
39
39
 
40
40
  requests_mock.get(
@@ -43,11 +43,16 @@ class TestGetSegmentsFromIdentifier:
43
43
  json=self.segments_response,
44
44
  )
45
45
 
46
- expected_response = {"SPGI": self.segments_response["segments"]}
46
+ expected_response = {
47
+ "results": {"SPGI": self.segments_response},
48
+ "errors": [
49
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
50
+ ],
51
+ }
47
52
 
48
53
  tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
49
54
  args = GetSegmentsFromIdentifiersArgs(
50
- identifiers=["SPGI"], segment_type=SegmentType.business
55
+ identifiers=["SPGI", "non-existent"], segment_type=SegmentType.business
51
56
  )
52
57
  response = tool.run(args.model_dump(mode="json"))
53
58
  assert response == expected_response
@@ -61,8 +66,10 @@ class TestGetSegmentsFromIdentifier:
61
66
 
62
67
  company_ids = [1, 2]
63
68
  expected_response = {
64
- "C_1": {"2021": self.segments_response["segments"]["2021"]},
65
- "C_2": {"2021": self.segments_response["segments"]["2021"]},
69
+ "results": {
70
+ "C_1": {"segments": {"2021": self.segments_response["segments"]["2021"]}},
71
+ "C_2": {"segments": {"2021": self.segments_response["segments"]["2021"]}},
72
+ }
66
73
  }
67
74
 
68
75
  for company_id in company_ids:
@@ -1,3 +1,6 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
1
4
  from strenum import StrEnum
2
5
 
3
6
 
@@ -7,3 +10,7 @@ class StatementType(StrEnum):
7
10
  balance_sheet = "balance_sheet"
8
11
  income_statement = "income_statement"
9
12
  cashflow = "cashflow"
13
+
14
+
15
+ class StatementsResp(BaseModel):
16
+ statements: dict[str, Any]
@@ -1,21 +1,16 @@
1
1
  from textwrap import dedent
2
2
  from typing import Literal, Type
3
3
 
4
- import numpy as np
5
- import pandas as pd
6
4
  from pydantic import BaseModel, Field
7
5
 
8
6
  from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
9
7
  from kfinance.client.models.date_and_period_models import PeriodType
10
8
  from kfinance.client.permission_models import Permission
11
- from kfinance.domains.companies.company_identifiers import (
12
- fetch_company_ids_from_identifiers,
13
- parse_identifiers,
14
- )
15
- from kfinance.domains.statements.statement_models import StatementType
9
+ from kfinance.domains.statements.statement_models import StatementsResp, StatementType
16
10
  from kfinance.integrations.tool_calling.tool_calling_models import (
17
11
  KfinanceTool,
18
12
  ToolArgsWithIdentifiers,
13
+ ToolRespWithErrors,
19
14
  )
20
15
 
21
16
 
@@ -29,6 +24,10 @@ class GetFinancialStatementFromIdentifiersArgs(ToolArgsWithIdentifiers):
29
24
  end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
30
25
 
31
26
 
27
+ class GetFinancialStatementFromIdentifiersResp(ToolRespWithErrors):
28
+ results: dict[str, StatementsResp]
29
+
30
+
32
31
  class GetFinancialStatementFromIdentifiers(KfinanceTool):
33
32
  name: str = "get_financial_statement_from_identifiers"
34
33
  description: str = dedent("""
@@ -59,23 +58,25 @@ class GetFinancialStatementFromIdentifiers(KfinanceTool):
59
58
  """Sample response:
60
59
 
61
60
  {
62
- 'SPGI': {
63
- 'Revenues': {'2020': 7442000000.0, '2021': 8243000000.0},
64
- 'Total Revenues': {'2020': 7442000000.0, '2021': 8243000000.0}
65
- }
61
+ 'results': {
62
+ 'SPGI': {
63
+ 'statements': {
64
+ '2020': {'Revenues': '7442000000.000000', 'Total Revenues': '7442000000.000000'},
65
+ '2021': {'Revenues': '8243000000.000000', 'Total Revenues': '8243000000.000000'}
66
+ }
67
+ }
68
+ },
69
+ 'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
66
70
  }
67
71
  """
68
72
  api_client = self.kfinance_client.kfinance_api_client
69
- parsed_identifiers = parse_identifiers(identifiers, api_client=api_client)
70
- identifiers_to_company_ids = fetch_company_ids_from_identifiers(
71
- identifiers=parsed_identifiers, api_client=api_client
72
- )
73
+ id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
73
74
 
74
75
  tasks = [
75
76
  Task(
76
77
  func=api_client.fetch_statement,
77
78
  kwargs=dict(
78
- company_id=company_id,
79
+ company_id=id_triple.company_id,
79
80
  statement_type=statement.value,
80
81
  period_type=period_type,
81
82
  start_year=start_year,
@@ -85,32 +86,29 @@ class GetFinancialStatementFromIdentifiers(KfinanceTool):
85
86
  ),
86
87
  result_key=identifier,
87
88
  )
88
- for identifier, company_id in identifiers_to_company_ids.items()
89
+ for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
89
90
  ]
90
91
 
91
- statement_responses = process_tasks_in_thread_pool_executor(
92
+ statement_responses: dict[str, StatementsResp] = process_tasks_in_thread_pool_executor(
92
93
  api_client=api_client, tasks=tasks
93
94
  )
94
95
 
95
- output = dict()
96
- for identifier, result in statement_responses.items():
97
- df = (
98
- pd.DataFrame(result["statements"])
99
- .apply(pd.to_numeric)
100
- .replace(np.nan, None)
101
- .transpose()
102
- )
103
- # If no date and multiple companies, only return the most recent value.
104
- # By default, we return 5 years of data, which can be too much when
105
- # returning data for many companies.
106
- if (
107
- start_year is None
108
- and end_year is None
109
- and start_quarter is None
110
- and end_quarter is None
111
- and len(identifiers) > 1
112
- ):
113
- df = df.tail(1)
114
- output[str(identifier)] = df.to_dict()
115
-
116
- return output
96
+ # If no date and multiple companies, only return the most recent value.
97
+ # By default, we return 5 years of data, which can be too much when
98
+ # returning data for many companies.
99
+ if (
100
+ start_year is None
101
+ and end_year is None
102
+ and start_quarter is None
103
+ and end_quarter is None
104
+ and len(statement_responses) > 1
105
+ ):
106
+ for statement_response in statement_responses.values():
107
+ most_recent_year = max(statement_response.statements.keys())
108
+ most_recent_year_data = statement_response.statements[most_recent_year]
109
+ statement_response.statements = {most_recent_year: most_recent_year_data}
110
+
111
+ output_model = GetFinancialStatementFromIdentifiersResp(
112
+ results=statement_responses, errors=list(id_triple_resp.errors.values())
113
+ )
114
+ return output_model.model_dump(mode="json")
@@ -23,8 +23,8 @@ class TestGetFinancialStatementFromIdentifiers:
23
23
  ):
24
24
  """
25
25
  GIVEN the GetFinancialLineItemFromIdentifiers tool
26
- WHEN we request the SPGI income statement
27
- THEN we get back the SPGI income statement
26
+ WHEN we request the income statement for SPGI and a non-existent company
27
+ THEN we get back the SPGI income statement and an error for the non-existent company.
28
28
  """
29
29
 
30
30
  requests_mock.get(
@@ -32,30 +32,59 @@ class TestGetFinancialStatementFromIdentifiers:
32
32
  json=self.statement_resp,
33
33
  )
34
34
  expected_response = {
35
- "SPGI": {
36
- "Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
37
- "Total Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
38
- }
35
+ "results": {
36
+ "SPGI": {
37
+ "statements": {
38
+ "2020": {
39
+ "Revenues": "7442000000.000000",
40
+ "Total Revenues": "7442000000.000000",
41
+ },
42
+ "2021": {
43
+ "Revenues": "8243000000.000000",
44
+ "Total Revenues": "8243000000.000000",
45
+ },
46
+ }
47
+ }
48
+ },
49
+ "errors": [
50
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
51
+ ],
39
52
  }
40
53
 
41
54
  tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
42
55
  args = GetFinancialStatementFromIdentifiersArgs(
43
- identifiers=["SPGI"], statement=StatementType.income_statement
56
+ identifiers=["SPGI", "non-existent"], statement=StatementType.income_statement
44
57
  )
45
58
  response = tool.run(args.model_dump(mode="json"))
46
59
  assert response == expected_response
47
60
 
48
61
  def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
49
62
  """
50
- GIVEN the GetFinancialLineItemFromIdentifiers tool
63
+ GIVEN the GetFinancialStatementFromIdentifiers tool
51
64
  WHEN we request most recent statement for multiple companies
52
65
  THEN we only get back the most recent statement for each company
53
66
  """
54
67
 
55
68
  company_ids = [1, 2]
56
69
  expected_response = {
57
- "C_1": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
58
- "C_2": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
70
+ "results": {
71
+ "C_1": {
72
+ "statements": {
73
+ "2021": {
74
+ "Revenues": "8243000000.000000",
75
+ "Total Revenues": "8243000000.000000",
76
+ }
77
+ }
78
+ },
79
+ "C_2": {
80
+ "statements": {
81
+ "2021": {
82
+ "Revenues": "8243000000.000000",
83
+ "Total Revenues": "8243000000.000000",
84
+ }
85
+ }
86
+ },
87
+ }
59
88
  }
60
89
 
61
90
  for company_id in company_ids:
@@ -1,17 +1,24 @@
1
- from kfinance.domains.line_items.line_item_tools import GetFinancialLineItemFromIdentifiers
2
- from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
1
+ BASE_PROMPT = f"""
2
+ You are an LLM designed to help financial analysts. Use the supplied tools to assist the user.
3
+ CRITICAL RULES FOR TOOL USAGE
3
4
 
5
+ Time Handling:
6
+ - Always select the most recent complete period when the user does not specify a time.
7
+ - Use the get_latest function to determine the latest annual year, latest completed quarter, and current date.
8
+ - For annual data, use the latest completed year. For quarterly data, use the latest completed quarter and year.
9
+ - If the user specifies a time period (year, quarter, or date range), use it exactly as provided.
10
+ - For relative time references (such as "3 quarters ago"), always use get_n_quarters_ago to resolve the correct year and quarter.
11
+ - For price or history tools, if the user does not specify a date range, use the most recent period as determined by get_latest.
12
+ - "Last year" or "last quarter" refers to the previous completed period from the current date.
13
+ - For quarterly data requests without specific quarters, assume the most recent completed quarter.
4
14
 
5
- BASE_PROMPT = f"""
6
- You are an agent that calls one or more tools to retrieve data to answer questions from
7
- financial analysts. Use the supplied tools to answer the user's questions.
15
+ Tool Selection:
16
+ - Use get_latest before any other tool when dates are ambiguous, unspecified, or when you need to determine the most recent period.
17
+ - Use get_n_quarters_ago for relative quarter references such as "3 quarters ago".
18
+ - Always make tool calls when financial data is requested—never skip them.
19
+ - For identifier resolution, use the exact identifiers provided by the user. Do not add or modify suffixes unless explicitly required.
8
20
 
9
- - Always use the `{GetLatest.model_fields["name"].default}` function when asked about the last or most recent quarter or
10
- when the time is unspecified in the question.
11
- - Try to use `{GetFinancialLineItemFromIdentifiers.model_fields["name"].default}` for questions about a company's
12
- finances.
13
- - If the tools do not respond with data that answers the question, then respond by saying that
14
- you don't have the data available.
15
- - Keep calling tools until you have the answer or the tool says the data is not available.
16
- - Label large numbers with "million" or "billion" and currency symbols if appropriate.
17
- """
21
+ Identifier Handling:
22
+ - Use the exact identifiers provided by the user. Do not add or modify suffixes such as ".PA" or ".DE" unless the user specifies the exchange or market.
23
+ - Never invent or guess identifiers. Only use those explicitly provided.
24
+ """
@@ -24,10 +24,10 @@ class TestGetEndpointsFromToolCallsWithGrounding:
24
24
  # truncated from the original
25
25
  resp_data = {"name": "S&P Global Inc.", "status": "Operating"}
26
26
  resp_endpoint = [
27
- "https://kfinance.kensho.com/api/v1/id/SPGI",
27
+ "https://kfinance.kensho.com/api/v1/ids",
28
28
  "https://kfinance.kensho.com/api/v1/info/21719",
29
29
  ]
30
- expected_resp = {"data": {"SPGI": resp_data}, "endpoint_urls": resp_endpoint}
30
+ expected_resp = {"data": {"results": {"SPGI": resp_data}}, "endpoint_urls": resp_endpoint}
31
31
 
32
32
  requests_mock.get(
33
33
  url=f"https://kfinance.kensho.com/api/v1/info/{SPGI_COMPANY_ID}",
@@ -1,7 +1,7 @@
1
- from typing import Annotated, Any, Literal, Type
1
+ from typing import Annotated, Any, Callable, Dict, Literal, Type
2
2
 
3
3
  from langchain_core.tools import BaseTool
4
- from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
4
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, model_serializer
5
5
 
6
6
  from kfinance.client.kfinance import Client
7
7
  from kfinance.client.permission_models import Permission
@@ -65,9 +65,7 @@ class KfinanceTool(BaseTool):
65
65
  Where feasible and useful, tools should use batch processing to parallelize
66
66
  requests, usually by allowing callers to pass in multiple identifiers.
67
67
  The usual processing order for functions with multiple identifiers is:
68
- - Parse string identifiers into `CompanyIdentifier`
69
- - batch fetch the required id for the api call (company/security/trading item id) via
70
- fetch_<relevant>_ids_from_identifiers
68
+ - batch fetch id triples via unified_fetch_id_triples
71
69
  - batch fetch the required info based on the ids via process_tasks_in_thread_pool_executor
72
70
  - format results for output
73
71
 
@@ -119,3 +117,24 @@ def convert_str_to_int(v: Any) -> Any:
119
117
  # ValidationError during deserialization unless they have been converted
120
118
  # to int.
121
119
  ValidQuarter = Annotated[Literal[1, 2, 3, 4], BeforeValidator(convert_str_to_int)]
120
+
121
+
122
+ class ToolRespWithErrors(BaseModel):
123
+ """A tool response with an `errors` field.
124
+
125
+ - `errors` is always the last field in the response.
126
+ - `errors` is only included if there is at least one error.
127
+ """
128
+
129
+ errors: list[str] = Field(default_factory=list)
130
+
131
+ @model_serializer(mode="wrap")
132
+ def serialize_model(self, handler: Callable) -> Dict[str, Any]:
133
+ """Make `errors` the last response field and only include if there is at least one error."""
134
+ data = handler(self)
135
+ errors = data.pop("errors")
136
+ # data = copy(data)
137
+ # data.keys()
138
+ if errors:
139
+ data["errors"] = errors
140
+ return data