mainsequence 3.11.2__tar.gz → 3.12.2__tar.gz

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 (133) hide show
  1. {mainsequence-3.11.2 → mainsequence-3.12.2}/PKG-INFO +1 -1
  2. mainsequence-3.12.2/mainsequence/ai/__init__.py +1 -0
  3. mainsequence-3.12.2/mainsequence/ai/base.py +314 -0
  4. mainsequence-3.12.2/mainsequence/ai/cli.py +54 -0
  5. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/models_tdag.py +19 -0
  6. mainsequence-3.12.2/mainsequence/dashboards/streamlit/components/asset_select.py +158 -0
  7. mainsequence-3.12.2/mainsequence/dashboards/streamlit/components/date_settings.py +50 -0
  8. mainsequence-3.12.2/mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  9. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/resource_factory/base_factory.py +1 -1
  10. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +1 -2
  11. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/PKG-INFO +1 -1
  12. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/SOURCES.txt +6 -3
  13. {mainsequence-3.11.2 → mainsequence-3.12.2}/pyproject.toml +1 -1
  14. mainsequence-3.11.2/mainsequence/virtualfundbuilder/agent_interface.py +0 -76
  15. mainsequence-3.11.2/mainsequence/virtualfundbuilder/contrib/apps/news_app.py +0 -493
  16. mainsequence-3.11.2/mainsequence/virtualfundbuilder/resource_factory/app_factory.py +0 -181
  17. {mainsequence-3.11.2 → mainsequence-3.12.2}/LICENSE +0 -0
  18. {mainsequence-3.11.2 → mainsequence-3.12.2}/README.md +0 -0
  19. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/__init__.py +0 -0
  20. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/__main__.py +0 -0
  21. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/cli/__init__.py +0 -0
  22. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/cli/api.py +0 -0
  23. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/cli/cli.py +0 -0
  24. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/cli/config.py +0 -0
  25. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/cli/ssh_utils.py +0 -0
  26. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/__init__.py +0 -0
  27. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/base.py +0 -0
  28. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  29. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  30. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/data_sources_interfaces/timescale.py +0 -0
  31. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/exceptions.py +0 -0
  32. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/models_helpers.py +0 -0
  33. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/models_vam.py +0 -0
  34. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/client/utils.py +0 -0
  35. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/__init__.py +0 -0
  36. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/__init__.py +0 -0
  37. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/config.toml +0 -0
  38. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  39. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/image_1_base64.txt +0 -0
  40. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/image_2_base64.txt +0 -0
  41. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/image_3_base64.txt +0 -0
  42. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/image_4_base64.txt +0 -0
  43. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/image_5_base64.txt +0 -0
  44. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  45. {mainsequence-3.11.2/mainsequence/dashboards/streamlit/core → mainsequence-3.12.2/mainsequence/dashboards/streamlit/components}/__init__.py +0 -0
  46. {mainsequence-3.11.2/mainsequence/dashboards/streamlit/pages → mainsequence-3.12.2/mainsequence/dashboards/streamlit/core}/__init__.py +0 -0
  47. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/core/theme.py +0 -0
  48. {mainsequence-3.11.2/mainsequence/instruments/interest_rates → mainsequence-3.12.2/mainsequence/dashboards/streamlit/pages}/__init__.py +0 -0
  49. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/dashboards/streamlit/scaffold.py +0 -0
  50. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instrumentation/__init__.py +0 -0
  51. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instrumentation/utils.py +0 -0
  52. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/__init__.py +0 -0
  53. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/data_interface/__init__.py +0 -0
  54. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/data_interface/data_interface.py +0 -0
  55. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/__init__.py +0 -0
  56. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/base_instrument.py +0 -0
  57. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/bond.py +0 -0
  58. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/european_option.py +0 -0
  59. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/interest_rate_swap.py +0 -0
  60. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/json_codec.py +0 -0
  61. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/knockout_fx_option.py +0 -0
  62. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/position.py +0 -0
  63. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/ql_fields.py +0 -0
  64. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/instruments/vanilla_fx_option.py +0 -0
  65. {mainsequence-3.11.2/mainsequence/instruments/interest_rates/etl → mainsequence-3.12.2/mainsequence/instruments/interest_rates}/__init__.py +0 -0
  66. {mainsequence-3.11.2/mainsequence/instruments/pricing_models → mainsequence-3.12.2/mainsequence/instruments/interest_rates/etl}/__init__.py +0 -0
  67. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/interest_rates/etl/curve_codec.py +0 -0
  68. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/interest_rates/etl/nodes.py +0 -0
  69. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/interest_rates/etl/registry.py +0 -0
  70. {mainsequence-3.11.2/mainsequence/virtualfundbuilder/contrib → mainsequence-3.12.2/mainsequence/instruments/pricing_models}/__init__.py +0 -0
  71. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/black_scholes.py +0 -0
  72. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/bond_pricer.py +0 -0
  73. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/fx_option_pricer.py +0 -0
  74. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/indices.py +0 -0
  75. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/indices_builders.py +0 -0
  76. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/knockout_fx_pricer.py +0 -0
  77. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/pricing_models/swap_pricer.py +0 -0
  78. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/settings.py +0 -0
  79. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/instruments/utils.py +0 -0
  80. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/logconf.py +0 -0
  81. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/__init__.py +0 -0
  82. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/__main__.py +0 -0
  83. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/config.py +0 -0
  84. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  85. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/build_operations.py +0 -0
  86. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
  87. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
  88. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
  89. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/data_nodes/utils.py +0 -0
  90. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/future_registry.py +0 -0
  91. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/tdag/utils.py +0 -0
  92. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/__init__.py +0 -0
  93. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/config_handling.py +0 -0
  94. {mainsequence-3.11.2/mainsequence/virtualfundbuilder/contrib/apps/report_styles → mainsequence-3.12.2/mainsequence/virtualfundbuilder/contrib}/__init__.py +0 -0
  95. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/__init__.py +0 -0
  96. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +0 -0
  97. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +0 -0
  98. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +0 -0
  99. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +0 -0
  100. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +0 -0
  101. {mainsequence-3.11.2/mainsequence/virtualfundbuilder/resource_factory → mainsequence-3.12.2/mainsequence/virtualfundbuilder/contrib/apps/report_styles}/__init__.py +0 -0
  102. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/report_styles/models.py +0 -0
  103. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/report_styles/utils.py +0 -0
  104. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +0 -0
  105. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +0 -0
  106. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +0 -0
  107. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +0 -0
  108. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +0 -0
  109. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +0 -0
  110. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +0 -0
  111. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +0 -0
  112. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +0 -0
  113. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +0 -0
  114. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/prices/__init__.py +0 -0
  115. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +0 -0
  116. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/prices/utils.py +0 -0
  117. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +0 -0
  118. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +0 -0
  119. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/data_nodes.py +0 -0
  120. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/enums.py +0 -0
  121. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/models.py +0 -0
  122. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/notebook_handling.py +0 -0
  123. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/portfolio_interface.py +0 -0
  124. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +0 -0
  125. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence/virtualfundbuilder/utils.py +0 -0
  126. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/dependency_links.txt +0 -0
  127. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/entry_points.txt +0 -0
  128. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/requires.txt +0 -0
  129. {mainsequence-3.11.2 → mainsequence-3.12.2}/mainsequence.egg-info/top_level.txt +0 -0
  130. {mainsequence-3.11.2 → mainsequence-3.12.2}/setup.cfg +0 -0
  131. {mainsequence-3.11.2 → mainsequence-3.12.2}/tests/test_agent.py +0 -0
  132. {mainsequence-3.11.2 → mainsequence-3.12.2}/tests/test_portfolio.py +0 -0
  133. {mainsequence-3.11.2 → mainsequence-3.12.2}/tests/test_replicator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.11.2
3
+ Version: 3.12.2
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -0,0 +1 @@
1
+ from .base import BaseAgentTool
@@ -0,0 +1,314 @@
1
+ """
2
+ base.py
3
+
4
+ Minimal, production-oriented client-side base for Agent Tools.
5
+
6
+ Requirements from Jose:
7
+ - Single public class: BaseAgentTool
8
+ - No registries / decorators / factories
9
+ - No HtmlApp
10
+ - No Jupyter/notebook helpers
11
+ - Keep _send_agent_tool_to_backend(payload) for AgentTool upsert
12
+ - Provide introspection for:
13
+ - input_schema
14
+ - output_schema (defined envelope with status/response/error)
15
+ - config_schema
16
+ - Provide canonical runtime entrypoint:
17
+ - run_and_response(): calls user run(), wraps into stable response envelope, then send_to_backend()
18
+
19
+ This file intentionally avoids any backend design advice.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import traceback
26
+ from abc import ABC, abstractmethod
27
+ from typing import Any
28
+
29
+ from pydantic import BaseModel
30
+
31
+ import mainsequence.client as msc
32
+ from mainsequence import logger
33
+
34
+ _REF_TEMPLATE = "#/$defs/{model}"
35
+
36
+
37
+
38
+
39
+ class BaseAgentTool(ABC):
40
+ """
41
+ Minimal base class for client-side Agent Tools.
42
+
43
+ Tool authors implement:
44
+ def run(self, *args, **kwargs): ...
45
+
46
+ Runners should call:
47
+ tool.run_and_response(*args, **kwargs)
48
+
49
+ The response is ALWAYS a dict:
50
+ {
51
+ "status": "OK" | "ERROR",
52
+ "response": <any JSON-serializable>,
53
+ "error": { ... } | None
54
+ }
55
+
56
+ This class also provides introspection helpers to build:
57
+ - input_schema
58
+ - output_schema (stable response envelope)
59
+ - config_schema
60
+ to match your backend AgentTool model fields.
61
+ """
62
+
63
+ # ---- metadata overrides ----
64
+ tool_description: str
65
+
66
+ # ---- config model (Pydantic) ----
67
+ configuration_class: type[BaseModel] #necessary on runtime
68
+
69
+ # ---- Optional strict IO models ----
70
+
71
+ output_model: type[BaseModel] | None = None
72
+
73
+
74
+
75
+ def __init__(self, configuration: Any | None = None):
76
+ """
77
+ Accepts:
78
+ - configuration as instance of configuration_class
79
+ - configuration as dict (will be parsed into configuration_class)
80
+ - configuration=None (will try configuration_class() if available)
81
+
82
+ If you don't use configuration, you can ignore it.
83
+ """
84
+ cfg_cls =self.__class__.configuration_class
85
+
86
+ if cfg_cls and isinstance(cfg_cls, type) and issubclass(cfg_cls, BaseModel):
87
+ if configuration is None:
88
+ # attempt default construction (will raise if required fields exist)
89
+ self.configuration = cfg_cls()
90
+ elif isinstance(configuration, cfg_cls):
91
+ self.configuration = configuration
92
+ elif isinstance(configuration, dict):
93
+ self.configuration = cfg_cls(**configuration)
94
+ else:
95
+ raise TypeError(
96
+ f"configuration must be {cfg_cls.__name__}, dict, or None; got {type(configuration)}"
97
+ )
98
+ else:
99
+ # No declared configuration class. Allow None or any BaseModel.
100
+ if configuration is not None and not isinstance(configuration, BaseModel):
101
+ raise TypeError(
102
+ "configuration must be a pydantic BaseModel instance or None "
103
+ "(or set configuration_class on the tool)."
104
+ )
105
+ self.configuration = configuration
106
+
107
+ # -----------------------
108
+ # Tool author implements
109
+ # -----------------------
110
+ @abstractmethod
111
+ def run(self, *args, **kwargs) -> Any:
112
+ raise NotImplementedError
113
+
114
+ # -----------------------
115
+ # Canonical runtime entrypoint
116
+ # -----------------------
117
+ def run_and_response(self, *args, **kwargs) -> dict[str, Any]:
118
+ """
119
+ Execute user run() and ALWAYS return a stable response envelope.
120
+
121
+ This is the method your runner should call.
122
+ """
123
+ try:
124
+ raw = self.run(*args, **kwargs)
125
+ envelope = self._normalize_user_return(raw)
126
+ except Exception as e:
127
+ envelope = {
128
+ "status": "ERROR",
129
+ "response": None,
130
+ "error": {
131
+ "message": str(e),
132
+ "error_type": e.__class__.__name__,
133
+ "details": None,
134
+ "traceback": traceback.format_exc(),
135
+ },
136
+ }
137
+
138
+ # Best-effort persistence hook (no hard assumptions)
139
+ try:
140
+ self.send_to_backend(envelope)
141
+ except Exception as e:
142
+ logger.warning(
143
+ "[%s] send_to_backend failed: %s", self.__class__.__name__, str(e), exc_info=True
144
+ )
145
+
146
+ return envelope
147
+
148
+ def _normalize_user_return(self, raw: Any) -> dict[str, Any]:
149
+ """
150
+ Normalize various user return types into the stable response envelope.
151
+
152
+ Supported:
153
+ - dict with 'status' and 'response'
154
+ - pydantic BaseModel -> becomes response (OK)
155
+ - any other value -> becomes response (OK)
156
+ """
157
+ if isinstance(raw, dict) and "status" in raw and "response" in raw:
158
+ status = raw.get("status", "OK")
159
+ if status not in ("OK", "ERROR"):
160
+ status = "OK"
161
+ response = self._to_jsonable(raw.get("response"))
162
+ err = raw.get("error")
163
+ err = self._to_jsonable(err) if err is not None else None
164
+ return {"status": status, "response": response, "error": err}
165
+
166
+ # If they returned a pydantic model, dump it
167
+ if isinstance(raw, BaseModel):
168
+ return {"status": "OK", "response": raw.model_dump(mode="json"), "error": None}
169
+
170
+ return {"status": "OK", "response": self._to_jsonable(raw), "error": None}
171
+
172
+ @staticmethod
173
+ def _to_jsonable(value: Any) -> Any:
174
+ """
175
+ Convert common Python/Pydantic objects into JSON-serializable structures.
176
+ """
177
+ if value is None:
178
+ return None
179
+ if isinstance(value, BaseModel):
180
+ return value.model_dump(mode="json")
181
+ if isinstance(value, dict):
182
+ return {str(k): BaseAgentTool._to_jsonable(v) for k, v in value.items()}
183
+ if isinstance(value, (list, tuple)):
184
+ return [BaseAgentTool._to_jsonable(v) for v in value]
185
+ if isinstance(value, set):
186
+ return [BaseAgentTool._to_jsonable(v) for v in sorted(value, key=lambda x: str(x))]
187
+ # Common "jsonable" objects
188
+ if hasattr(value, "isoformat"):
189
+ try:
190
+ return value.isoformat()
191
+ except Exception as e:
192
+ logger.exception(f"value {value} is not jsonable:")
193
+ raise e
194
+ return value
195
+
196
+ # -----------------------
197
+ # Persistence hook (override if you want)
198
+ # -----------------------
199
+ def send_to_backend(self, envelope: dict[str, Any]) -> None:
200
+ """
201
+ Best-effort hook to persist the tool response.
202
+
203
+ Default behavior:
204
+ - If SDK helper exists: mainsequence.client.models_tdag.add_tool_response_to_jobrun -> use it
205
+ - Else: store envelope as JSON artifact and attach via add_output()
206
+
207
+ You can override this if you have a preferred mechanism.
208
+ """
209
+
210
+ # If your SDK provides a direct helper, prefer it.
211
+
212
+ job_run_id=os.getenv("JOB_RUN_ID", None)
213
+ if job_run_id is None:
214
+ raise Exception("JOB_RUN_ID environment variable not set")
215
+
216
+ msc.AgentTool.add_tool_response_to_jobrun(job_run_id=job_run_id, response=envelope) # type: ignore
217
+
218
+
219
+ # -----------------------
220
+ # Tool metadata + schema introspection
221
+ # -----------------------
222
+ @classmethod
223
+ def agent_tool_payload(cls, attributes: dict[str, Any] | None = None) -> dict[str, Any]:
224
+ """
225
+ Build a dict that matches your backend AgentTool model fields:
226
+ slug, name, description, entrypoint,
227
+ input_schema, output_schema, config_schema,
228
+ attributes
229
+ """
230
+ return {
231
+ "description": cls.get_tool_description(),
232
+ "output_schema": cls.get_output_schema(),
233
+ "config_schema": cls.get_config_schema(),
234
+ }
235
+
236
+ @classmethod
237
+ def register_to_backend(cls, attributes: dict[str, Any] | None = None) -> None:
238
+ """
239
+ Explicit registration/upsert of this tool metadata.
240
+ (No registries, no automatic side effects.)
241
+ """
242
+ payload=cls.agent_tool_payload(attributes=attributes)
243
+ msc.AgentTool.update_or_create(**payload) # type: ignore
244
+
245
+
246
+
247
+ @classmethod
248
+ def get_tool_name(cls) -> str:
249
+ if isinstance(getattr(cls, "tool_name", None), str) and cls.tool_name:
250
+ return cls.tool_name
251
+ return cls.__name__
252
+
253
+
254
+
255
+
256
+
257
+ @classmethod
258
+ def get_config_schema(cls) -> dict[str, Any] | None:
259
+ cfg_cls = getattr(cls, "configuration_class", None)
260
+ if cfg_cls and isinstance(cfg_cls, type) and issubclass(cfg_cls, BaseModel):
261
+ return cfg_cls.model_json_schema(ref_template=_REF_TEMPLATE)
262
+ return None
263
+
264
+
265
+ @classmethod
266
+ def get_output_schema(cls) -> dict[str, Any]:
267
+ """
268
+ Output schema is always a stable response envelope:
269
+ {"status": "OK"|"ERROR", "response": ..., "error": ...}
270
+
271
+ If output_model is set, response is typed accordingly.
272
+ """
273
+ defs: dict[str, Any] = {}
274
+
275
+ # Build "response" schema
276
+ out_model = getattr(cls, "output_model", None)
277
+ if out_model and isinstance(out_model, type) and issubclass(out_model, BaseModel):
278
+ response_schema = out_model.model_json_schema(ref_template=_REF_TEMPLATE)
279
+ if "$defs" in response_schema:
280
+ defs.update(response_schema.pop("$defs"))
281
+ else:
282
+ response_schema = {}
283
+
284
+ # error schema (always the same shape)
285
+ error_schema: dict[str, Any] = {
286
+ "title": "ToolError",
287
+ "type": "object",
288
+ "properties": {
289
+ "message": {"type": "string"},
290
+ "error_type": {"type": ["string", "null"]},
291
+ "details": {"type": ["object", "null"]},
292
+ "traceback": {"type": ["string", "null"]},
293
+ },
294
+ "required": ["message"],
295
+ "additionalProperties": True,
296
+ }
297
+
298
+ envelope: dict[str, Any] = {
299
+ "title": f"{cls.__name__}Output",
300
+ "type": "object",
301
+ "properties": {
302
+ "status": {"type": "string", "enum": ["OK", "ERROR"]},
303
+ "response": {"anyOf": [response_schema, {"type": "null"}]},
304
+ "error": {"anyOf": [error_schema, {"type": "null"}]},
305
+ },
306
+ "required": ["status", "response", "error"],
307
+ "additionalProperties": False,
308
+ }
309
+
310
+ if defs:
311
+ envelope["$defs"] = defs
312
+
313
+ return envelope
314
+
@@ -0,0 +1,54 @@
1
+ # cli.py
2
+ import importlib
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+
8
+ def load_class(entry_point: str):
9
+ entry_point = entry_point.strip()
10
+ if ":" in entry_point: # preferred standard
11
+ module_path, class_name = entry_point.split(":", 1)
12
+ else: # also accept your requested dotted form
13
+ module_path, class_name = entry_point.rsplit(".", 1)
14
+
15
+ module = importlib.import_module(module_path)
16
+ return getattr(module, class_name)
17
+
18
+
19
+ def read_configuration() -> dict:
20
+ # Priority: argv[1] -> $CONFIGURATION -> stdin (if piped)
21
+ if len(sys.argv) >= 2 and sys.argv[1].strip():
22
+ raw = sys.argv[1]
23
+ else:
24
+ raw = os.getenv("TOOL_CONFIGURATION")
25
+ if (raw is None or not raw.strip()) and not sys.stdin.isatty():
26
+ raw = sys.stdin.read()
27
+
28
+ if raw is None or not raw.strip():
29
+ return {}
30
+
31
+ cfg = json.loads(raw)
32
+ if not isinstance(cfg, dict):
33
+ raise SystemExit("CONFIGURATION must be a JSON object (dict).")
34
+ return cfg
35
+
36
+
37
+ def main() -> None:
38
+ entry_point = os.getenv("TOOL_ENTRY_POINT")
39
+ if not entry_point:
40
+ raise SystemExit("Missing TOOL_ENTRY_POINT env var.")
41
+
42
+ ToolClass = load_class(entry_point)
43
+ cfg = read_configuration()
44
+
45
+ # What you asked for:
46
+
47
+ tool = ToolClass(**cfg)
48
+ tool.run_and_response()
49
+
50
+
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -2689,6 +2689,25 @@ class Constant(BasePydanticModel, BaseObjectOrm):
2689
2689
  return created_constants
2690
2690
 
2691
2691
 
2692
+
2693
+ # AI Models
2694
+
2695
+ class AgentTool(BasePydanticModel, BaseObjectOrm):
2696
+ """
2697
+ Represents the actual tool metadata that an agent needs to know.
2698
+ """
2699
+ slug: str
2700
+ name: str = Field(max_length=255)
2701
+ description:str= None
2702
+
2703
+ entrypoint: str
2704
+ # JSONFields
2705
+ output_schema: dict[str, Any] | None = None
2706
+ config_schema: dict[str, Any] | None = None
2707
+
2708
+
2709
+
2710
+
2692
2711
  SessionDataSource = PodDataSource()
2693
2712
  SessionDataSource.set_remote_db()
2694
2713
 
@@ -0,0 +1,158 @@
1
+ # dashboards/streamlit/components/asset_select.py
2
+ """
3
+ import mainsequence.dashboards.streamlit.components.asset_select
4
+
5
+
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import streamlit as st
12
+
13
+ import mainsequence.client as msc
14
+
15
+
16
+ @st.cache_data(show_spinner=False)
17
+ def _search_assets(q: str) -> list[dict[str, Any]]:
18
+ """
19
+ Search MainSequence assets by current_snapshot.name or unique_identifier.
20
+ Returns [{'id', 'label', 'instance'}] with unique IDs.
21
+ """
22
+ if not q or len(q.strip()) < 3:
23
+ return []
24
+ out: list[dict[str, Any]] = []
25
+ seen = set()
26
+
27
+ try:
28
+ by_name = msc.Asset.filter(current_snapshot__name__contains=q.strip())
29
+ except Exception:
30
+ by_name = []
31
+
32
+ try:
33
+ by_uid = msc.Asset.filter(unique_identifier__contains=q.strip())
34
+ except Exception:
35
+ by_uid = []
36
+
37
+ for a in list(by_name or []) + list(by_uid or []):
38
+ try:
39
+ aid = getattr(a, "id", None)
40
+ if aid is None or aid in seen:
41
+ continue
42
+ uid = getattr(a, "unique_identifier", None)
43
+ snap = getattr(getattr(a, "current_snapshot", None), "name", None)
44
+ label = f"{uid or '—'} · {snap or '—'} (id={aid})"
45
+ out.append({"id": aid, "label": label, "instance": a})
46
+ seen.add(aid)
47
+ except Exception:
48
+ continue
49
+ return out
50
+
51
+
52
+ def sidebar_asset_single_select(
53
+ *,
54
+ title: str = "Find asset",
55
+ key_prefix: str = "asset_select",
56
+ min_chars: int = 3,
57
+ ) -> Any | None:
58
+ """
59
+ Streamlit sidebar widget:
60
+ - Text search (cached)
61
+ - Single-select of results
62
+ - Returns the loaded Asset instance and keeps it in session until cleared.
63
+ - Only changes the loaded asset when "Load asset" is pressed.
64
+ Session keys:
65
+ • {key_prefix}_loaded_id
66
+ • {key_prefix}_loaded_instance
67
+ • {key_prefix}_q, {key_prefix}_results, {key_prefix}_sel (UI state)
68
+ """
69
+ loaded_id_key = f"{key_prefix}_loaded_id"
70
+ loaded_inst_key = f"{key_prefix}_loaded_instance"
71
+ results_key = f"{key_prefix}_results"
72
+ select_key = f"{key_prefix}_sel"
73
+ query_key = f"{key_prefix}_q"
74
+
75
+ with st.sidebar:
76
+ st.markdown(f"### {title}")
77
+ st.caption(f"Type at least **{min_chars}** characters to search.")
78
+
79
+ # Show the currently loaded asset (if any)
80
+ loaded_id = st.session_state.get(loaded_id_key)
81
+ loaded_inst = st.session_state.get(loaded_inst_key)
82
+ if loaded_inst is not None:
83
+ try:
84
+ uid = getattr(loaded_inst, "unique_identifier", None)
85
+ snap = getattr(getattr(loaded_inst, "current_snapshot", None), "name", None)
86
+ st.caption(f"Loaded asset: **{uid or '—'} · {snap or '—'} (id={loaded_id})**")
87
+ except Exception:
88
+ st.caption(f"Loaded asset: id={loaded_id}")
89
+
90
+ # Search box
91
+ def _do_search_from_state(prefix: str, min_chars_local: int) -> None:
92
+ q_local = st.session_state.get(f"{prefix}_q", "")
93
+ if q_local and len(q_local.strip()) >= min_chars_local:
94
+ st.session_state[f"{prefix}_results"] = _search_assets(q_local.strip())
95
+ else:
96
+ st.session_state.pop(f"{prefix}_results", None)
97
+
98
+ q = st.text_input(
99
+ "Search assets",
100
+ placeholder="e.g. BONOS 2030 or MXN:... UID",
101
+ key=query_key,
102
+ on_change=_do_search_from_state,
103
+ args=(key_prefix, min_chars),
104
+ )
105
+
106
+ if st.button("Search", key=f"{key_prefix}_btn") and q and len(q.strip()) >= min_chars:
107
+ st.session_state[results_key] = _search_assets(q.strip())
108
+
109
+ assets = st.session_state.get(results_key, [])
110
+ if q and len(q.strip()) < min_chars:
111
+ st.caption(f"Keep typing… need **{min_chars}+** characters to search.")
112
+
113
+ if assets:
114
+ id_to_label = {a["id"]: a["label"] for a in assets}
115
+ id_to_inst = {a["id"]: a["instance"] for a in assets}
116
+ # Preselect currently loaded id if visible in the options
117
+ default_index = 0
118
+ options = list(id_to_label.keys())
119
+ if loaded_id in options:
120
+ default_index = options.index(loaded_id)
121
+
122
+ selected_id = st.selectbox(
123
+ "Select an asset",
124
+ options=options,
125
+ index=default_index if options else 0,
126
+ format_func=lambda x: id_to_label.get(x, str(x)),
127
+ key=select_key,
128
+ )
129
+
130
+ col_load, col_clear = st.columns([3, 2])
131
+ with col_load:
132
+ if st.button(
133
+ "Load asset", type="primary", width='stretch', key=f"{key_prefix}_load"
134
+ ):
135
+ inst = id_to_inst.get(selected_id)
136
+ # Persist loaded selection
137
+ st.session_state[loaded_id_key] = (
138
+ int(selected_id) if selected_id is not None else None
139
+ )
140
+ st.session_state[loaded_inst_key] = inst
141
+ return inst
142
+
143
+ with col_clear:
144
+ if st.button("Clear", width='stretch', key=f"{key_prefix}_clear"):
145
+ # Clear UI + loaded asset
146
+ st.session_state.pop(query_key, None)
147
+ st.session_state.pop(results_key, None)
148
+ st.session_state.pop(select_key, None)
149
+ st.session_state.pop(loaded_id_key, None)
150
+ st.session_state.pop(loaded_inst_key, None)
151
+ st.rerun()
152
+
153
+ # Persisted behavior: if an asset is already loaded, return it on every rerun
154
+ loaded_inst = st.session_state.get(loaded_inst_key)
155
+ if loaded_inst is not None:
156
+ return loaded_inst
157
+
158
+ return None
@@ -0,0 +1,50 @@
1
+ # dashboards/streamlit/components/date_settings.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import os
6
+
7
+ import streamlit as st
8
+ from dashboards.components.date_selector import date_selector
9
+
10
+
11
+ def sidebar_date_settings(
12
+ *,
13
+ date_label: str = "Valuation date",
14
+ session_cfg_key: str = "position_cfg_mem",
15
+ cfg_field: str = "valuation_date",
16
+ date_key: str = "valuation_date_input",
17
+ date_help: str | None = None,
18
+ use_last_curve_label: str = "Use last available curve",
19
+ use_last_curve_key: str = "use_last_obs_curve",
20
+ use_last_curve_help: str | None = None,
21
+ env_var_key: str = "USE_LAST_OBSERVATION_MS_INSTRUMENT",
22
+ ) -> tuple[dt.date, bool]:
23
+ """
24
+ Sidebar date settings: valuation date selector + "use last available curve" toggle.
25
+ Persists date in session cfg and toggles env var for downstream curve construction.
26
+ Returns (valuation_date, use_last_curve).
27
+ """
28
+ valuation_date = date_selector(
29
+ label=date_label,
30
+ session_cfg_key=session_cfg_key,
31
+ cfg_field=cfg_field,
32
+ key=date_key,
33
+ help=date_help,
34
+ )
35
+
36
+ default_use_last = st.session_state.get(
37
+ use_last_curve_key,
38
+ os.environ.get(env_var_key, "1").lower() in ("1", "true", "yes", "y", "on"),
39
+ )
40
+ use_last_curve = st.checkbox(
41
+ use_last_curve_label,
42
+ value=default_use_last,
43
+ key=use_last_curve_key,
44
+ help=use_last_curve_help,
45
+ )
46
+
47
+ # Set the process env var so downstream curve-building respects it in this run.
48
+ os.environ[env_var_key] = "True" if use_last_curve else "False"
49
+
50
+ return valuation_date, use_last_curve
@@ -239,7 +239,7 @@ def send_resource_to_backend(resource_class, attributes: dict | None = None):
239
239
  if merged_definitions:
240
240
  final_json_schema["$defs"] = merged_definitions
241
241
 
242
- resource_config = DynamicResource.create(
242
+ resource_config = DynamicResource.update_or_create(
243
243
  name=resource_class.__name__,
244
244
  type=resource_class.TYPE.value,
245
245
  object_signature=final_json_schema,
@@ -166,8 +166,7 @@ def _get_class_source_code(cls):
166
166
  return None
167
167
 
168
168
 
169
- SIGNAL_CLASS_REGISTRY = SIGNAL_CLASS_REGISTRY if "SIGNAL_CLASS_REGISTRY" in globals() else {}
170
-
169
+ SIGNAL_CLASS_REGISTRY = globals().get("SIGNAL_CLASS_REGISTRY", {})
171
170
 
172
171
  def register_signal_class(name=None, register_in_agent=True):
173
172
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.11.2
3
+ Version: 3.12.2
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement