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.
- {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/METADATA +1 -1
- {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/RECORD +46 -45
- kfinance/CHANGELOG.md +6 -0
- kfinance/client/fetch.py +26 -17
- kfinance/client/kfinance.py +17 -18
- kfinance/client/meta_classes.py +2 -2
- kfinance/client/tests/test_fetch.py +36 -24
- kfinance/client/tests/test_objects.py +112 -120
- kfinance/conftest.py +49 -5
- kfinance/domains/business_relationships/business_relationship_tools.py +30 -19
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -15
- kfinance/domains/capitalizations/capitalization_models.py +1 -1
- kfinance/domains/capitalizations/capitalization_tools.py +41 -24
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +38 -13
- kfinance/domains/companies/company_identifiers.py +0 -175
- kfinance/domains/companies/company_models.py +98 -5
- kfinance/domains/companies/company_tools.py +33 -29
- kfinance/domains/companies/tests/test_company_tools.py +11 -4
- kfinance/domains/competitors/competitor_tools.py +21 -21
- kfinance/domains/competitors/tests/test_competitor_tools.py +21 -7
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +38 -26
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +27 -26
- kfinance/domains/earnings/earning_tools.py +54 -47
- kfinance/domains/earnings/tests/test_earnings_tools.py +58 -63
- kfinance/domains/line_items/line_item_tools.py +29 -36
- kfinance/domains/line_items/tests/test_line_item_tools.py +23 -5
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +15 -0
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -38
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +22 -9
- kfinance/domains/prices/price_models.py +9 -9
- kfinance/domains/prices/price_tools.py +49 -38
- kfinance/domains/prices/tests/test_price_tools.py +52 -36
- kfinance/domains/segments/segment_models.py +7 -0
- kfinance/domains/segments/segment_tools.py +37 -20
- kfinance/domains/segments/tests/test_segment_tools.py +13 -6
- kfinance/domains/statements/statement_models.py +7 -0
- kfinance/domains/statements/statement_tools.py +38 -40
- kfinance/domains/statements/tests/test_statement_tools.py +39 -10
- kfinance/integrations/tool_calling/prompts.py +21 -14
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +2 -2
- kfinance/integrations/tool_calling/tool_calling_models.py +24 -5
- kfinance/version.py +16 -3
- {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/WHEEL +0 -0
- {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.1.dist-info}/licenses/LICENSE +0 -0
- {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 = {
|
|
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(
|
|
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
|
|
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
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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-
|
|
123
|
-
"open": {"value": "
|
|
124
|
-
"high": {"value": "
|
|
125
|
-
"low": {"value": "
|
|
126
|
-
"close": {"value": "
|
|
127
|
-
"volume": {"value": "
|
|
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
|
-
"
|
|
133
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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(
|
|
98
|
+
and len(segments_responses) > 1
|
|
82
99
|
):
|
|
83
100
|
for segments_response in segments_responses.values():
|
|
84
|
-
most_recent_year = max(segments_response
|
|
85
|
-
most_recent_year_data = segments_response
|
|
86
|
-
segments_response
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
"
|
|
65
|
-
|
|
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.
|
|
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
|
-
'
|
|
63
|
-
'
|
|
64
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
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
|
|
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
|
-
"
|
|
58
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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/
|
|
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
|
-
-
|
|
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
|