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.
- infra_mvp/base_client.py +29 -0
- infra_mvp/base_server.py +68 -0
- infra_mvp/monitoring/__init__.py +15 -0
- infra_mvp/monitoring/metrics.py +185 -0
- infra_mvp/stream/README.md +56 -0
- infra_mvp/stream/__init__.py +14 -0
- infra_mvp/stream/__main__.py +101 -0
- infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
- infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
- infra_mvp/stream/agents/reference/blocks/action.json +170 -0
- infra_mvp/stream/agents/reference/blocks/button.json +66 -0
- infra_mvp/stream/agents/reference/blocks/date.json +65 -0
- infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
- infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
- infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
- infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
- infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
- infra_mvp/stream/agents/reference/blocks/table.json +56 -0
- infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
- infra_mvp/stream/app.py +49 -0
- infra_mvp/stream/container.py +112 -0
- infra_mvp/stream/schemas/__init__.py +16 -0
- infra_mvp/stream/schemas/agent.py +24 -0
- infra_mvp/stream/schemas/interaction.py +28 -0
- infra_mvp/stream/schemas/session.py +30 -0
- infra_mvp/stream/server.py +321 -0
- infra_mvp/stream/services/__init__.py +12 -0
- infra_mvp/stream/services/agent_service.py +40 -0
- infra_mvp/stream/services/event_converter.py +83 -0
- infra_mvp/stream/services/session_service.py +247 -0
- yera/__init__.py +50 -1
- yera/agents/__init__.py +2 -0
- yera/agents/context.py +41 -0
- yera/agents/dataclasses.py +69 -0
- yera/agents/decorator.py +207 -0
- yera/agents/discovery.py +124 -0
- yera/agents/typing/__init__.py +0 -0
- yera/agents/typing/coerce.py +408 -0
- yera/agents/typing/utils.py +19 -0
- yera/agents/typing/validate.py +206 -0
- yera/cli.py +377 -0
- yera/config/__init__.py +1 -0
- yera/config/config_utils.py +164 -0
- yera/config/function_config.py +55 -0
- yera/config/logging.py +18 -0
- yera/config/tool_config.py +8 -0
- yera/config2/__init__.py +8 -0
- yera/config2/dataclasses.py +534 -0
- yera/config2/keyring.py +270 -0
- yera/config2/paths.py +28 -0
- yera/config2/read.py +113 -0
- yera/config2/setup.py +109 -0
- yera/config2/setup_handlers/__init__.py +1 -0
- yera/config2/setup_handlers/anthropic.py +126 -0
- yera/config2/setup_handlers/azure.py +236 -0
- yera/config2/setup_handlers/base.py +125 -0
- yera/config2/setup_handlers/llama_cpp.py +205 -0
- yera/config2/setup_handlers/ollama.py +157 -0
- yera/config2/setup_handlers/openai.py +137 -0
- yera/config2/write.py +87 -0
- yera/dsl/__init__.py +0 -0
- yera/dsl/functions.py +94 -0
- yera/dsl/struct.py +20 -0
- yera/dsl/workspace.py +79 -0
- yera/events/__init__.py +57 -0
- yera/events/blocks/__init__.py +68 -0
- yera/events/blocks/action.py +57 -0
- yera/events/blocks/bar_chart.py +92 -0
- yera/events/blocks/base/__init__.py +20 -0
- yera/events/blocks/base/base.py +166 -0
- yera/events/blocks/base/chart.py +288 -0
- yera/events/blocks/base/layout.py +111 -0
- yera/events/blocks/buttons.py +37 -0
- yera/events/blocks/columns.py +26 -0
- yera/events/blocks/container.py +24 -0
- yera/events/blocks/date_picker.py +50 -0
- yera/events/blocks/exit.py +39 -0
- yera/events/blocks/form.py +24 -0
- yera/events/blocks/input_echo.py +22 -0
- yera/events/blocks/input_request.py +31 -0
- yera/events/blocks/line_chart.py +97 -0
- yera/events/blocks/markdown.py +67 -0
- yera/events/blocks/slider.py +54 -0
- yera/events/blocks/spinner.py +55 -0
- yera/events/blocks/system_prompt.py +22 -0
- yera/events/blocks/table.py +291 -0
- yera/events/models/__init__.py +39 -0
- yera/events/models/block_data.py +112 -0
- yera/events/models/in_event.py +7 -0
- yera/events/models/out_event.py +75 -0
- yera/events/runtime.py +187 -0
- yera/events/stream.py +91 -0
- yera/models/__init__.py +0 -0
- yera/models/data_classes.py +20 -0
- yera/models/llm_atlas_proxy.py +44 -0
- yera/models/llm_context.py +99 -0
- yera/models/llm_interfaces/__init__.py +0 -0
- yera/models/llm_interfaces/anthropic.py +153 -0
- yera/models/llm_interfaces/aws_bedrock.py +14 -0
- yera/models/llm_interfaces/azure_openai.py +143 -0
- yera/models/llm_interfaces/base.py +26 -0
- yera/models/llm_interfaces/interface_registry.py +74 -0
- yera/models/llm_interfaces/llama_cpp.py +136 -0
- yera/models/llm_interfaces/mock.py +29 -0
- yera/models/llm_interfaces/ollama_interface.py +118 -0
- yera/models/llm_interfaces/open_ai.py +150 -0
- yera/models/llm_workspace.py +19 -0
- yera/models/model_atlas.py +139 -0
- yera/models/model_definition.py +38 -0
- yera/models/model_factory.py +33 -0
- yera/opaque/__init__.py +9 -0
- yera/opaque/base.py +20 -0
- yera/opaque/decorator.py +8 -0
- yera/opaque/markdown.py +57 -0
- yera/opaque/opaque_function.py +25 -0
- yera/tools/__init__.py +29 -0
- yera/tools/atlas_tool.py +20 -0
- yera/tools/base.py +24 -0
- yera/tools/decorated_tool.py +18 -0
- yera/tools/decorator.py +35 -0
- yera/tools/tool_atlas.py +51 -0
- yera/tools/tool_utils.py +361 -0
- yera/ui/dist/404.html +1 -0
- yera/ui/dist/__next.__PAGE__.txt +10 -0
- yera/ui/dist/__next._full.txt +23 -0
- yera/ui/dist/__next._head.txt +6 -0
- yera/ui/dist/__next._index.txt +5 -0
- yera/ui/dist/__next._tree.txt +7 -0
- yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_buildManifest.js +11 -0
- yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_clientMiddlewareManifest.json +1 -0
- yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_ssgManifest.js +1 -0
- yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
- yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
- yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
- yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
- yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
- yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
- yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
- yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
- yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
- yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
- yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
- yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
- yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
- yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
- yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
- yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
- yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
- yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
- yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
- yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
- yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
- yera/ui/dist/_not-found/__next._full.txt +14 -0
- yera/ui/dist/_not-found/__next._head.txt +6 -0
- yera/ui/dist/_not-found/__next._index.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.txt +4 -0
- yera/ui/dist/_not-found/__next._tree.txt +2 -0
- yera/ui/dist/_not-found.html +1 -0
- yera/ui/dist/_not-found.txt +14 -0
- yera/ui/dist/agent-icon.svg +3 -0
- yera/ui/dist/favicon.ico +0 -0
- yera/ui/dist/file.svg +1 -0
- yera/ui/dist/globe.svg +1 -0
- yera/ui/dist/index.html +1 -0
- yera/ui/dist/index.txt +23 -0
- yera/ui/dist/logo/full_logo.png +0 -0
- yera/ui/dist/logo/rune_logo.png +0 -0
- yera/ui/dist/logo/rune_logo_borderless.png +0 -0
- yera/ui/dist/logo/text_logo.png +0 -0
- yera/ui/dist/next.svg +1 -0
- yera/ui/dist/send.png +0 -0
- yera/ui/dist/send_single.png +0 -0
- yera/ui/dist/vercel.svg +1 -0
- yera/ui/dist/window.svg +1 -0
- yera/utils/__init__.py +1 -0
- yera/utils/path_utils.py +38 -0
- yera-0.2.1.dist-info/METADATA +65 -0
- yera-0.2.1.dist-info/RECORD +190 -0
- {yera-0.1.1.dist-info → yera-0.2.1.dist-info}/WHEEL +1 -1
- yera-0.2.1.dist-info/entry_points.txt +2 -0
- yera-0.1.1.dist-info/METADATA +0 -11
- 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
|
+
)
|