yera 0.1.1__py3-none-any.whl → 0.2.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.
Files changed (192) hide show
  1. infra_mvp/base_client.py +29 -0
  2. infra_mvp/base_server.py +68 -0
  3. infra_mvp/monitoring/__init__.py +15 -0
  4. infra_mvp/monitoring/metrics.py +185 -0
  5. infra_mvp/stream/README.md +56 -0
  6. infra_mvp/stream/__init__.py +14 -0
  7. infra_mvp/stream/__main__.py +101 -0
  8. infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
  9. infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
  10. infra_mvp/stream/agents/reference/blocks/action.json +170 -0
  11. infra_mvp/stream/agents/reference/blocks/button.json +66 -0
  12. infra_mvp/stream/agents/reference/blocks/date.json +65 -0
  13. infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
  14. infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
  15. infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
  16. infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
  17. infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
  18. infra_mvp/stream/agents/reference/blocks/table.json +56 -0
  19. infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
  20. infra_mvp/stream/app.py +49 -0
  21. infra_mvp/stream/container.py +112 -0
  22. infra_mvp/stream/schemas/__init__.py +16 -0
  23. infra_mvp/stream/schemas/agent.py +24 -0
  24. infra_mvp/stream/schemas/interaction.py +28 -0
  25. infra_mvp/stream/schemas/session.py +30 -0
  26. infra_mvp/stream/server.py +321 -0
  27. infra_mvp/stream/services/__init__.py +12 -0
  28. infra_mvp/stream/services/agent_service.py +40 -0
  29. infra_mvp/stream/services/event_converter.py +83 -0
  30. infra_mvp/stream/services/session_service.py +247 -0
  31. yera/__init__.py +50 -1
  32. yera/agents/__init__.py +2 -0
  33. yera/agents/context.py +41 -0
  34. yera/agents/dataclasses.py +69 -0
  35. yera/agents/decorator.py +207 -0
  36. yera/agents/discovery.py +124 -0
  37. yera/agents/typing/__init__.py +0 -0
  38. yera/agents/typing/coerce.py +408 -0
  39. yera/agents/typing/utils.py +19 -0
  40. yera/agents/typing/validate.py +206 -0
  41. yera/cli.py +377 -0
  42. yera/config/__init__.py +1 -0
  43. yera/config/config_utils.py +164 -0
  44. yera/config/function_config.py +55 -0
  45. yera/config/logging.py +18 -0
  46. yera/config/tool_config.py +8 -0
  47. yera/config2/__init__.py +8 -0
  48. yera/config2/dataclasses.py +534 -0
  49. yera/config2/keyring.py +270 -0
  50. yera/config2/paths.py +28 -0
  51. yera/config2/read.py +113 -0
  52. yera/config2/setup.py +109 -0
  53. yera/config2/setup_handlers/__init__.py +1 -0
  54. yera/config2/setup_handlers/anthropic.py +126 -0
  55. yera/config2/setup_handlers/azure.py +236 -0
  56. yera/config2/setup_handlers/base.py +125 -0
  57. yera/config2/setup_handlers/llama_cpp.py +205 -0
  58. yera/config2/setup_handlers/ollama.py +157 -0
  59. yera/config2/setup_handlers/openai.py +137 -0
  60. yera/config2/write.py +87 -0
  61. yera/dsl/__init__.py +0 -0
  62. yera/dsl/functions.py +94 -0
  63. yera/dsl/struct.py +20 -0
  64. yera/dsl/workspace.py +79 -0
  65. yera/events/__init__.py +57 -0
  66. yera/events/blocks/__init__.py +68 -0
  67. yera/events/blocks/action.py +57 -0
  68. yera/events/blocks/bar_chart.py +92 -0
  69. yera/events/blocks/base/__init__.py +20 -0
  70. yera/events/blocks/base/base.py +166 -0
  71. yera/events/blocks/base/chart.py +288 -0
  72. yera/events/blocks/base/layout.py +111 -0
  73. yera/events/blocks/buttons.py +37 -0
  74. yera/events/blocks/columns.py +26 -0
  75. yera/events/blocks/container.py +24 -0
  76. yera/events/blocks/date_picker.py +50 -0
  77. yera/events/blocks/exit.py +39 -0
  78. yera/events/blocks/form.py +24 -0
  79. yera/events/blocks/input_echo.py +22 -0
  80. yera/events/blocks/input_request.py +31 -0
  81. yera/events/blocks/line_chart.py +97 -0
  82. yera/events/blocks/markdown.py +67 -0
  83. yera/events/blocks/slider.py +54 -0
  84. yera/events/blocks/spinner.py +55 -0
  85. yera/events/blocks/system_prompt.py +22 -0
  86. yera/events/blocks/table.py +291 -0
  87. yera/events/models/__init__.py +39 -0
  88. yera/events/models/block_data.py +112 -0
  89. yera/events/models/in_event.py +7 -0
  90. yera/events/models/out_event.py +75 -0
  91. yera/events/runtime.py +187 -0
  92. yera/events/stream.py +91 -0
  93. yera/models/__init__.py +0 -0
  94. yera/models/data_classes.py +20 -0
  95. yera/models/llm_atlas_proxy.py +44 -0
  96. yera/models/llm_context.py +99 -0
  97. yera/models/llm_interfaces/__init__.py +0 -0
  98. yera/models/llm_interfaces/anthropic.py +153 -0
  99. yera/models/llm_interfaces/aws_bedrock.py +14 -0
  100. yera/models/llm_interfaces/azure_openai.py +143 -0
  101. yera/models/llm_interfaces/base.py +26 -0
  102. yera/models/llm_interfaces/interface_registry.py +74 -0
  103. yera/models/llm_interfaces/llama_cpp.py +136 -0
  104. yera/models/llm_interfaces/mock.py +29 -0
  105. yera/models/llm_interfaces/ollama_interface.py +118 -0
  106. yera/models/llm_interfaces/open_ai.py +150 -0
  107. yera/models/llm_workspace.py +19 -0
  108. yera/models/model_atlas.py +139 -0
  109. yera/models/model_definition.py +38 -0
  110. yera/models/model_factory.py +33 -0
  111. yera/opaque/__init__.py +9 -0
  112. yera/opaque/base.py +20 -0
  113. yera/opaque/decorator.py +8 -0
  114. yera/opaque/markdown.py +57 -0
  115. yera/opaque/opaque_function.py +25 -0
  116. yera/tools/__init__.py +29 -0
  117. yera/tools/atlas_tool.py +20 -0
  118. yera/tools/base.py +24 -0
  119. yera/tools/decorated_tool.py +18 -0
  120. yera/tools/decorator.py +35 -0
  121. yera/tools/tool_atlas.py +51 -0
  122. yera/tools/tool_utils.py +361 -0
  123. yera/ui/dist/404.html +1 -0
  124. yera/ui/dist/__next.__PAGE__.txt +10 -0
  125. yera/ui/dist/__next._full.txt +23 -0
  126. yera/ui/dist/__next._head.txt +6 -0
  127. yera/ui/dist/__next._index.txt +5 -0
  128. yera/ui/dist/__next._tree.txt +7 -0
  129. yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_buildManifest.js +11 -0
  130. yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_clientMiddlewareManifest.json +1 -0
  131. yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_ssgManifest.js +1 -0
  132. yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
  133. yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
  134. yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
  135. yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
  136. yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
  137. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  138. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  139. yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
  140. yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
  141. yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
  142. yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
  143. yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
  144. yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
  145. yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
  146. yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
  147. yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
  148. yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
  149. yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  150. yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
  151. yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
  152. yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  153. yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  154. yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  155. yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
  156. yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
  157. yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
  158. yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
  159. yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  160. yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  161. yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
  162. yera/ui/dist/_not-found/__next._full.txt +14 -0
  163. yera/ui/dist/_not-found/__next._head.txt +6 -0
  164. yera/ui/dist/_not-found/__next._index.txt +5 -0
  165. yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
  166. yera/ui/dist/_not-found/__next._not-found.txt +4 -0
  167. yera/ui/dist/_not-found/__next._tree.txt +2 -0
  168. yera/ui/dist/_not-found.html +1 -0
  169. yera/ui/dist/_not-found.txt +14 -0
  170. yera/ui/dist/agent-icon.svg +3 -0
  171. yera/ui/dist/favicon.ico +0 -0
  172. yera/ui/dist/file.svg +1 -0
  173. yera/ui/dist/globe.svg +1 -0
  174. yera/ui/dist/index.html +1 -0
  175. yera/ui/dist/index.txt +23 -0
  176. yera/ui/dist/logo/full_logo.png +0 -0
  177. yera/ui/dist/logo/rune_logo.png +0 -0
  178. yera/ui/dist/logo/rune_logo_borderless.png +0 -0
  179. yera/ui/dist/logo/text_logo.png +0 -0
  180. yera/ui/dist/next.svg +1 -0
  181. yera/ui/dist/send.png +0 -0
  182. yera/ui/dist/send_single.png +0 -0
  183. yera/ui/dist/vercel.svg +1 -0
  184. yera/ui/dist/window.svg +1 -0
  185. yera/utils/__init__.py +1 -0
  186. yera/utils/path_utils.py +38 -0
  187. yera-0.2.1.dist-info/METADATA +65 -0
  188. yera-0.2.1.dist-info/RECORD +190 -0
  189. {yera-0.1.1.dist-info → yera-0.2.1.dist-info}/WHEEL +1 -1
  190. yera-0.2.1.dist-info/entry_points.txt +2 -0
  191. yera-0.1.1.dist-info/METADATA +0 -11
  192. yera-0.1.1.dist-info/RECORD +0 -4
@@ -0,0 +1,236 @@
1
+ # ruff: noqa: T201
2
+ """Configuration and credentials import handler for Azure Foundry."""
3
+
4
+ from azure.identity import DefaultAzureCredential
5
+ from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
6
+ from azure.mgmt.resource import SubscriptionClient
7
+
8
+ from yera.config2.dataclasses import CredentialsMap, LLMConfig, ModelRegistry
9
+ from yera.config2.keyring import DevKeyring
10
+ from yera.config2.setup_handlers.base import BaseProviderSetup
11
+
12
+
13
+ def _discover_azure_openai_accounts(credential: DefaultAzureCredential) -> list[dict]:
14
+ subscription_client = SubscriptionClient(credential)
15
+
16
+ accounts = []
17
+
18
+ for subscription in subscription_client.subscriptions.list():
19
+ sub_id = subscription.subscription_id
20
+
21
+ cog_client = CognitiveServicesManagementClient(credential, sub_id)
22
+
23
+ for account in cog_client.accounts.list():
24
+ if account.kind == "OpenAI":
25
+ resource_group = account.id.split("/")[4]
26
+
27
+ accounts.append(
28
+ {
29
+ "account_name": account.name,
30
+ "endpoint": account.properties.endpoint,
31
+ "subscription_id": sub_id,
32
+ "resource_group": resource_group,
33
+ }
34
+ )
35
+
36
+ return accounts
37
+
38
+
39
+ def _list_deployments(credential: DefaultAzureCredential, account: dict) -> list[dict]:
40
+ client = CognitiveServicesManagementClient(credential, account["subscription_id"])
41
+
42
+ deployments = client.deployments.list(
43
+ resource_group_name=account["resource_group"],
44
+ account_name=account["account_name"],
45
+ )
46
+
47
+ return [
48
+ {
49
+ "name": d.name,
50
+ "model": d.properties.model.name,
51
+ "version": d.properties.model.version,
52
+ }
53
+ for d in deployments
54
+ ]
55
+
56
+
57
+ def _get_api_key(credential: DefaultAzureCredential, account: dict) -> str:
58
+ client = CognitiveServicesManagementClient(credential, account["subscription_id"])
59
+
60
+ keys = client.accounts.list_keys(
61
+ resource_group_name=account["resource_group"],
62
+ account_name=account["account_name"],
63
+ )
64
+
65
+ return keys.key1
66
+
67
+
68
+ class AzureSetup(BaseProviderSetup):
69
+ """Setup handler for Azure OpenAI credentials and deployments.
70
+
71
+ Manages automatic discovery and manual configuration of Azure OpenAI
72
+ resources. Supports automatic credential detection via Azure CLI login
73
+ (DefaultAzureCredential) and manual entry fallback.
74
+ """
75
+
76
+ def __init__(self):
77
+ """Initialise the Azure OpenAI setup handler."""
78
+ super().__init__("azure")
79
+
80
+ def detect_creds(self) -> dict[str, str] | None:
81
+ """Automatically detect Azure OpenAI credentials via Azure CLI.
82
+
83
+ Uses DefaultAzureCredential to authenticate and discover Azure OpenAI
84
+ accounts across all accessible subscriptions. Automatically retrieves
85
+ API keys for discovered accounts. Requires you to log in with the cli
86
+ beforehand.
87
+
88
+ Returns:
89
+ Dictionary containing account_name, endpoint, subscription_id,
90
+ resource_group, and api_key if a single account is found.
91
+ None if no accounts are found or authentication fails.
92
+
93
+ Raises:
94
+ RuntimeError: If multiple Azure OpenAI accounts are found.
95
+
96
+ Note:
97
+ Requires user to be logged in via 'az login' and have appropriate
98
+ permissions to list accounts and retrieve API keys.
99
+ """
100
+ credential = DefaultAzureCredential()
101
+ accounts = _discover_azure_openai_accounts(credential=credential)
102
+
103
+ if len(accounts) == 0:
104
+ return None
105
+
106
+ if len(accounts) > 1:
107
+ raise RuntimeError("We support just the one account for now")
108
+
109
+ account = accounts[0]
110
+ account["api_key"] = _get_api_key(credential=credential, account=account)
111
+ return account
112
+
113
+ def ask_for_creds(self) -> dict[str, str] | None:
114
+ """Prompt user to manually enter Azure OpenAI credentials.
115
+
116
+ Interactively requests all required Azure OpenAI configuration
117
+ including account name, endpoint, subscription ID, resource group,
118
+ and API key.
119
+
120
+ Returns:
121
+ Dictionary with account_name, endpoint, subscription_id,
122
+ resource_group, and api_key if user provides credentials.
123
+ None if user declines to enter credentials.
124
+ """
125
+ ask = self._confirm_ask_for_creds()
126
+ if not ask:
127
+ return None
128
+
129
+ fields = [
130
+ "account_name",
131
+ "endpoint",
132
+ "subscription_id",
133
+ "resource_group",
134
+ "api_key",
135
+ ]
136
+ field_list = ", ".join(fields)
137
+ print(f"To get everything set up we will need your Azure account {field_list}")
138
+ account = {}
139
+ for field in fields:
140
+ print("Please enter your " + field + ":")
141
+ account[field] = input(" > ")
142
+
143
+ return account
144
+
145
+ def validate_creds(self, creds: dict[str, str]) -> None:
146
+ """Validate Azure OpenAI credential format and completeness.
147
+
148
+ Checks that all required fields are present and validates format
149
+ of endpoint URL, API key length, and subscription ID structure.
150
+
151
+ Args:
152
+ creds: Dictionary containing Azure OpenAI credentials.
153
+
154
+ Raises:
155
+ ValueError: If required fields are missing, endpoint format is
156
+ invalid, or API key appears incomplete.
157
+ """
158
+ # Check required fields are present
159
+ required_fields = [
160
+ "account_name",
161
+ "endpoint",
162
+ "subscription_id",
163
+ "resource_group",
164
+ "api_key",
165
+ ]
166
+ missing = [f for f in required_fields if f not in creds or not creds[f]]
167
+
168
+ if missing:
169
+ raise ValueError(f"Missing required fields: {', '.join(missing)}")
170
+
171
+ # Validate endpoint format
172
+ endpoint = creds["endpoint"]
173
+ if not endpoint.startswith("https://"):
174
+ raise ValueError("Endpoint must start with 'https://'")
175
+
176
+ if not (
177
+ ".openai.azure.com" in endpoint
178
+ or ".api.cognitive.microsoft.com" in endpoint
179
+ ):
180
+ print(" Warning: Endpoint doesn't match expected Azure OpenAI format")
181
+
182
+ # Validate API key format (basic check)
183
+ api_key = creds["api_key"]
184
+ if len(api_key) < 20:
185
+ raise ValueError(
186
+ "API key appears too short. Please ensure you've copied the complete key."
187
+ )
188
+
189
+ # Validate subscription_id format (should be a GUID)
190
+ subscription_id = creds["subscription_id"]
191
+ if len(subscription_id) != 36 or subscription_id.count("-") != 4:
192
+ print(" Warning: Subscription ID doesn't look like a valid GUID")
193
+
194
+ def fetch_models(self, creds_map: CredentialsMap) -> ModelRegistry:
195
+ """Fetch available model deployments from Azure OpenAI account.
196
+
197
+ Uses Azure Management SDK to list all model deployments in the
198
+ configured Azure OpenAI account and creates LLMConfig entries
199
+ for each deployment.
200
+
201
+ Args:
202
+ creds_map: Credentials map containing Azure credential labels.
203
+
204
+ Returns:
205
+ ModelRegistry containing LLMConfig entries for all discovered
206
+ model deployments.
207
+
208
+ Note:
209
+ Requires DefaultAzureCredential authentication to access the
210
+ Azure Management API.
211
+ """
212
+ account = {
213
+ k: DevKeyring.get(f"providers.{v}")
214
+ for k, v in creds_map.providers["azure"].items()
215
+ }
216
+
217
+ credential = DefaultAzureCredential()
218
+ deployments = _list_deployments(credential, account)
219
+
220
+ def _make_model_config(d: dict) -> LLMConfig:
221
+ return LLMConfig(
222
+ id=f"azure.{d['name']}".lower(),
223
+ display_name=d["model"],
224
+ credentials="azure",
225
+ provider="azure",
226
+ interface="azure-sdk",
227
+ model_id=d["name"],
228
+ )
229
+
230
+ model_configs = [_make_model_config(d) for d in deployments]
231
+ names = " \n * ".join(m.display_name for m in model_configs)
232
+ print(f" Adding {len(model_configs)} LLMs from Azure:\n * {names}")
233
+
234
+ return ModelRegistry(
235
+ llm={m.id: m for m in model_configs},
236
+ )
@@ -0,0 +1,125 @@
1
+ # ruff: noqa: T201
2
+ """Module for the base class of provider setup handlers."""
3
+
4
+ from abc import ABC, abstractmethod
5
+
6
+ from yera.config2.dataclasses import (
7
+ CredentialsMap,
8
+ ModelRegistry,
9
+ YeraConfig,
10
+ )
11
+ from yera.config2.keyring import DevKeyring
12
+
13
+
14
+ class BaseProviderSetup(ABC):
15
+ """Abstract base class for AI provider setup handlers.
16
+
17
+ Defines the interface for credential detection, validation, and model
18
+ fetching across different AI providers. Subclasses implement provider-
19
+ specific logic while inheriting common setup workflows.
20
+ """
21
+
22
+ def __init__(self, provider_name: str) -> None:
23
+ """Initialise the provider setup handler.
24
+
25
+ Args:
26
+ provider_name: Name of the provider (e.g., "anthropic", "openai").
27
+ """
28
+ self.provider_name = provider_name
29
+
30
+ def _confirm_ask_for_creds(self) -> bool:
31
+ print(
32
+ f" Would you like to add your {self.provider_name.title()} API key? (y/n)"
33
+ )
34
+
35
+ choice = input(" > ")
36
+ while choice not in ["y", "n"]:
37
+ print(" Please enter either 'y' or 'n'.")
38
+ choice = input(" > ")
39
+
40
+ return choice == "y"
41
+
42
+ @abstractmethod
43
+ def detect_creds(self) -> dict[str, str] | None:
44
+ """Detect credentials from environment."""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def ask_for_creds(self) -> dict[str, str] | None:
49
+ """Prompt user for credentials."""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def validate_creds(self, creds: dict[str, str]) -> None:
54
+ """Validate credential format."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def fetch_models(self, creds_map: CredentialsMap) -> ModelRegistry:
59
+ """Fetch available models using provider API."""
60
+ pass
61
+
62
+ def automatic_setup(self) -> YeraConfig | None:
63
+ """Attempt automatic setup by detecting credentials from pre-existing data.
64
+
65
+ These can be environment variables and configuration files.
66
+
67
+ Returns:
68
+ YeraConfig with detected credentials and fetched models, or None
69
+ if automatic detection fails.
70
+ """
71
+ print(f"[AUTO]: Getting credentials for {self.provider_name}")
72
+ try:
73
+ creds = self.detect_creds()
74
+ return self._setup_and_config(creds)
75
+ except Exception as e:
76
+ print(
77
+ f" An error occurred while attempting set up "
78
+ f"for {self.provider_name} ({type(e).__name__}: {e})"
79
+ )
80
+ print(" Skipping...")
81
+ return None
82
+
83
+ def interactive_setup(self) -> YeraConfig | None:
84
+ """Perform interactive setup by prompting user to input information.
85
+
86
+ Returns:
87
+ YeraConfig with user-provided credentials and fetched models, or
88
+ None if user declines to provide credentials.
89
+ """
90
+ try:
91
+ creds = self.ask_for_creds()
92
+ return self._setup_and_config(creds)
93
+ except Exception as e:
94
+ print(
95
+ f" An error occurred while attempting to set up "
96
+ f"for {self.provider_name} ({type(e).__name__}: {e})"
97
+ )
98
+ print(" Skipping...")
99
+ return None
100
+
101
+ def _setup_and_config(self, creds: dict[str, str] | None) -> YeraConfig | None:
102
+ if creds is None:
103
+ return None
104
+
105
+ self.validate_creds(creds)
106
+
107
+ credentials_map = self._add_to_keyring_and_make_map(creds)
108
+ try:
109
+ models = self.fetch_models(credentials_map)
110
+ except Exception as e:
111
+ print(f" Warning: Could not fetch {self.provider_name} models: {e}")
112
+ print(" Continuing with empty model list.")
113
+ models = ModelRegistry()
114
+
115
+ print(f" Config is updated for {self.provider_name}")
116
+ return YeraConfig(credentials=credentials_map, models=models)
117
+
118
+ def _add_to_keyring_and_make_map(self, creds: dict[str, str]) -> CredentialsMap:
119
+ """Standard keyring storage (same for all providers)."""
120
+ for k, v in creds.items():
121
+ key = f"providers.{self.provider_name}.{k}"
122
+ print(f" Adding {key} to dev keyring.")
123
+ DevKeyring.set(key, v)
124
+ map_dict = {k: f"{self.provider_name}.{k}" for k in creds}
125
+ return CredentialsMap(providers={self.provider_name: map_dict})
@@ -0,0 +1,205 @@
1
+ # ruff: noqa: T201
2
+ """Configuration import handler for Llama-cpp."""
3
+
4
+ import json
5
+ import os
6
+ import struct
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ import gguf
11
+ from numpy import memmap
12
+ from platformdirs import user_data_dir
13
+
14
+ from yera.config2.dataclasses import (
15
+ CredentialsMap,
16
+ LLMConfig,
17
+ ModelRegistry,
18
+ )
19
+ from yera.config2.keyring import DevKeyring
20
+ from yera.config2.setup_handlers.base import BaseProviderSetup
21
+
22
+
23
+ def _decode_value(value: memmap | bytes) -> str | int:
24
+ if hasattr(value, "tobytes"):
25
+ value = value.tobytes()
26
+
27
+ if isinstance(value, bytes):
28
+ # Try to decode as UTF-8 string first
29
+ try:
30
+ decoded = value.decode("utf-8")
31
+ # If it's printable and non-empty, prefer string interpretation
32
+ if decoded.isprintable() and decoded.strip():
33
+ return decoded
34
+ except UnicodeDecodeError:
35
+ pass
36
+
37
+ # For 4-byte values that aren't valid strings, try integer unpacking
38
+ if len(value) == 4:
39
+ try:
40
+ return struct.unpack("<I", value)[0]
41
+ except struct.error:
42
+ pass
43
+
44
+ return "[could not decode]"
45
+
46
+
47
+ def _get_context_length(meta: dict) -> int | str:
48
+ keys = [k for k in meta if k.endswith(".context_length")]
49
+ if len(keys) == 1:
50
+ return meta[keys[0]]
51
+ return "Unknown"
52
+
53
+
54
+ def _find_project_root(
55
+ srcs: tuple[str, ...] | None = None,
56
+ markers: tuple[str, ...] = ("pyproject.toml", ".git"),
57
+ ) -> Path:
58
+ if not srcs:
59
+ srcs = (str(Path.cwd()),)
60
+
61
+ path = Path(srcs[0]).resolve()
62
+
63
+ for parent in [path, *path.parents]:
64
+ if any((parent / marker).exists() for marker in markers):
65
+ return parent
66
+
67
+ return path
68
+
69
+
70
+ def _get_models_dir(
71
+ location: Literal["local", "global"],
72
+ ) -> Path:
73
+ if location == "local":
74
+ return _find_project_root() / "models"
75
+ if location == "global":
76
+ return Path(user_data_dir("yera")) / "models"
77
+ raise ValueError(f"Unknown location: {location}")
78
+
79
+
80
+ class LlamaCppSetup(BaseProviderSetup):
81
+ """Setup handler for llama-cpp.
82
+
83
+ Manages finding the model directory, finding ggufs, extracting their
84
+ metadata and creating a catalogue json that maps IDs to local files.
85
+ """
86
+
87
+ def __init__(self):
88
+ """Initialise the LlamaCpp setup handler."""
89
+ super().__init__("llama-cpp")
90
+
91
+ def detect_creds(self) -> dict[str, str] | None:
92
+ """Detect LlamaCpp models directory.
93
+
94
+ The priority is
95
+ 1. YERA_MODELS_DIR env variable
96
+ 2. ./models
97
+ 3. ~/.local/yera/models
98
+
99
+ Returns:
100
+ Dictionary containing the model directory path, otherwise
101
+ None if no model directory is found,
102
+
103
+ """
104
+ # 1. Check environment variable first
105
+ if env_dir := os.getenv("YERA_MODEL_DIR"):
106
+ return {"models_dir": env_dir}
107
+
108
+ # 2. Try local project directory
109
+ local_dir = _get_models_dir("local")
110
+ if local_dir.exists():
111
+ return {"models_dir": str(local_dir)}
112
+
113
+ # 3. Fall back to global user directory
114
+ global_dir = _get_models_dir("global")
115
+ return {"models_dir": str(global_dir)}
116
+
117
+ def ask_for_creds(self) -> dict[str, str] | None:
118
+ """Prompt the user to enter their models directory manually.
119
+
120
+ Returns:
121
+ Dictionary containing the model directory path, otherwise
122
+ None if user enters an invalid path.
123
+
124
+ """
125
+ print("Please enter the path to your models directory.")
126
+ models_dir = Path(input(" > "))
127
+ return {"models_dir": str(models_dir)}
128
+
129
+ def validate_creds(self, creds: dict[str, str]) -> None:
130
+ """Validate the path to the models directory.
131
+
132
+ Args:
133
+ creds: Dictionary containing the model directory path.
134
+
135
+ Raises:
136
+ FileNotFoundError: Raised if the path does not exist.
137
+
138
+ """
139
+ models_dir = Path(creds["models_dir"])
140
+ if not models_dir.exists():
141
+ raise FileNotFoundError(models_dir)
142
+
143
+ def _make_model_cfg(self, model: dict[str, str]) -> LLMConfig:
144
+ provider = model.get("organization", "unknown")
145
+ gguf_path = Path(model["gguf_path"])
146
+
147
+ model_id = gguf_path.stem.replace(".", "p")
148
+ return LLMConfig(
149
+ id=f"llamacpp.{provider}.{model_id}".lower(),
150
+ display_name=model["name"],
151
+ credentials=self.provider_name,
152
+ provider=provider,
153
+ interface="llama-cpp",
154
+ model_id=model_id,
155
+ )
156
+
157
+ def fetch_models(self, creds_map: CredentialsMap) -> ModelRegistry:
158
+ """Fetch information from ggufs in models directory and build registry.
159
+
160
+ Args:
161
+ creds_map: Dictionary containing the model directory path.
162
+
163
+ Returns:
164
+ ModelRegistry containing configurations for all the models in the
165
+ models directory.
166
+
167
+ """
168
+ creds = {
169
+ k: DevKeyring.get(f"providers.{v}")
170
+ for k, v in creds_map.providers[self.provider_name].items()
171
+ }
172
+
173
+ models_dir = Path(creds["models_dir"])
174
+
175
+ catalogue = {}
176
+
177
+ model_configs = []
178
+ for gguf_path in list(models_dir.rglob("*.gguf")):
179
+ reader = gguf.GGUFReader(gguf_path)
180
+
181
+ data = {"gguf_path": str(gguf_path)}
182
+ other = {}
183
+
184
+ for k, v in reader.fields.items():
185
+ val = v.parts[v.data[0]]
186
+ if k.startswith("general."):
187
+ data[v.name[8:]] = _decode_value(val)
188
+ else:
189
+ other[v.name] = _decode_value(val)
190
+
191
+ data["quant"] = gguf_path.stem.split("-")[-1]
192
+ data["context_length"] = _get_context_length(other)
193
+
194
+ model_cfg = self._make_model_cfg(data)
195
+ catalogue[model_cfg.id] = str(gguf_path)
196
+ model_configs.append(model_cfg)
197
+
198
+ (models_dir / "catalogue.json").write_text(json.dumps(catalogue, indent=2))
199
+
200
+ names = " \n * ".join(m.display_name for m in model_configs)
201
+ print(f" Adding {len(model_configs)} LLMs from {models_dir}:\n * {names}")
202
+
203
+ return ModelRegistry(
204
+ llm={m.id: m for m in model_configs},
205
+ )