volknode 0.2.5__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.
- volknode/__init__.py +0 -0
- volknode/main.py +214 -0
- volknode/src/core/__init__.py +0 -0
- volknode/src/core/result.py +26 -0
- volknode/src/core/validation.py +20 -0
- volknode/src/csr/__init__.py +0 -0
- volknode/src/csr/csr_service.py +47 -0
- volknode/src/csr/key_pair_generator.py +34 -0
- volknode/src/dependency_resolver.py +33 -0
- volknode/src/dependency_resolver_test.py +56 -0
- volknode/src/domain/__init__.py +0 -0
- volknode/src/domain/csr_credentials.py +7 -0
- volknode/src/domain/llm_model.py +12 -0
- volknode/src/domain/llm_node_certificate.py +6 -0
- volknode/src/domain/nexus_certificate.py +6 -0
- volknode/src/domain/running_vllm_config.py +7 -0
- volknode/src/http_client/.gitignore +23 -0
- volknode/src/http_client/README.md +124 -0
- volknode/src/http_client/ai_suite_client/__init__.py +8 -0
- volknode/src/http_client/ai_suite_client/api/__init__.py +1 -0
- volknode/src/http_client/ai_suite_client/api/default/__init__.py +1 -0
- volknode/src/http_client/ai_suite_client/api/default/change_password.py +197 -0
- volknode/src/http_client/ai_suite_client/api/default/converse.py +178 -0
- volknode/src/http_client/ai_suite_client/api/default/create_personal_access_token.py +193 -0
- volknode/src/http_client/ai_suite_client/api/default/create_user.py +207 -0
- volknode/src/http_client/ai_suite_client/api/default/delay_converse_indefinitely_for_prompt.py +127 -0
- volknode/src/http_client/ai_suite_client/api/default/delete_conversation.py +185 -0
- volknode/src/http_client/ai_suite_client/api/default/delete_user.py +189 -0
- volknode/src/http_client/ai_suite_client/api/default/dislike_message.py +182 -0
- volknode/src/http_client/ai_suite_client/api/default/edit_sent_message.py +196 -0
- volknode/src/http_client/ai_suite_client/api/default/get_app_context.py +150 -0
- volknode/src/http_client/ai_suite_client/api/default/get_conversation_by_id.py +182 -0
- volknode/src/http_client/ai_suite_client/api/default/get_conversations.py +190 -0
- volknode/src/http_client/ai_suite_client/api/default/get_my_profile.py +158 -0
- volknode/src/http_client/ai_suite_client/api/default/is_live.py +141 -0
- volknode/src/http_client/ai_suite_client/api/default/is_ready.py +141 -0
- volknode/src/http_client/ai_suite_client/api/default/like_message.py +182 -0
- volknode/src/http_client/ai_suite_client/api/default/list_personal_access_tokens.py +158 -0
- volknode/src/http_client/ai_suite_client/api/default/login_with_credentials.py +193 -0
- volknode/src/http_client/ai_suite_client/api/default/register_llm_node.py +208 -0
- volknode/src/http_client/ai_suite_client/api/default/rename_conversation.py +206 -0
- volknode/src/http_client/ai_suite_client/api/default/reset_app.py +107 -0
- volknode/src/http_client/ai_suite_client/api/default/retry_ai_response.py +175 -0
- volknode/src/http_client/ai_suite_client/api/default/revoke_personal_access_token.py +196 -0
- volknode/src/http_client/ai_suite_client/api/default/stop_indefinite_converse_answer_for_prompt.py +127 -0
- volknode/src/http_client/ai_suite_client/api/default/view_llm_nodes.py +201 -0
- volknode/src/http_client/ai_suite_client/api/default/view_users.py +190 -0
- volknode/src/http_client/ai_suite_client/client.py +271 -0
- volknode/src/http_client/ai_suite_client/errors.py +14 -0
- volknode/src/http_client/ai_suite_client/models/__init__.py +109 -0
- volknode/src/http_client/ai_suite_client/models/ai_assistant_started_typing_event.py +76 -0
- volknode/src/http_client/ai_suite_client/models/ai_retry_started_event.py +76 -0
- volknode/src/http_client/ai_suite_client/models/app_context_response.py +122 -0
- volknode/src/http_client/ai_suite_client/models/available_model.py +92 -0
- volknode/src/http_client/ai_suite_client/models/business_constraints.py +76 -0
- volknode/src/http_client/ai_suite_client/models/change_password_request.py +92 -0
- volknode/src/http_client/ai_suite_client/models/conversation.py +84 -0
- volknode/src/http_client/ai_suite_client/models/conversation_with_messages.py +110 -0
- volknode/src/http_client/ai_suite_client/models/conversations_response.py +118 -0
- volknode/src/http_client/ai_suite_client/models/converse_request.py +86 -0
- volknode/src/http_client/ai_suite_client/models/converse_response_server_event.py +182 -0
- volknode/src/http_client/ai_suite_client/models/converse_response_server_event_event_type.py +12 -0
- volknode/src/http_client/ai_suite_client/models/create_personal_access_token_request.py +101 -0
- volknode/src/http_client/ai_suite_client/models/create_user_request.py +120 -0
- volknode/src/http_client/ai_suite_client/models/delay_converse_indefinitely_for_prompt_request.py +76 -0
- volknode/src/http_client/ai_suite_client/models/edit_sent_message_request.py +76 -0
- volknode/src/http_client/ai_suite_client/models/edit_sent_message_response_server_event.py +166 -0
- volknode/src/http_client/ai_suite_client/models/edit_sent_message_response_server_event_event_type.py +11 -0
- volknode/src/http_client/ai_suite_client/models/error.py +101 -0
- volknode/src/http_client/ai_suite_client/models/error_event.py +88 -0
- volknode/src/http_client/ai_suite_client/models/list_personal_access_tokens_response.py +94 -0
- volknode/src/http_client/ai_suite_client/models/logged_in_user.py +100 -0
- volknode/src/http_client/ai_suite_client/models/login_with_credentials_request.py +84 -0
- volknode/src/http_client/ai_suite_client/models/message.py +120 -0
- volknode/src/http_client/ai_suite_client/models/message_author.py +8 -0
- volknode/src/http_client/ai_suite_client/models/message_edited_event.py +76 -0
- volknode/src/http_client/ai_suite_client/models/my_profile_response.py +122 -0
- volknode/src/http_client/ai_suite_client/models/new_conversation_started_event.py +76 -0
- volknode/src/http_client/ai_suite_client/models/no_available_models_event.py +84 -0
- volknode/src/http_client/ai_suite_client/models/pagination.py +84 -0
- volknode/src/http_client/ai_suite_client/models/permissions.py +92 -0
- volknode/src/http_client/ai_suite_client/models/personal_access_token_created_successfully.py +92 -0
- volknode/src/http_client/ai_suite_client/models/personal_access_token_list_item.py +120 -0
- volknode/src/http_client/ai_suite_client/models/prompted_message_saved_event.py +76 -0
- volknode/src/http_client/ai_suite_client/models/register_llm_node_request.py +124 -0
- volknode/src/http_client/ai_suite_client/models/register_llm_node_response.py +100 -0
- volknode/src/http_client/ai_suite_client/models/rename_conversation_request.py +76 -0
- volknode/src/http_client/ai_suite_client/models/reply_chunk.py +100 -0
- volknode/src/http_client/ai_suite_client/models/reply_chunk_content.py +88 -0
- volknode/src/http_client/ai_suite_client/models/reply_chunk_content_content_type.py +7 -0
- volknode/src/http_client/ai_suite_client/models/retry_ai_response_response_server_event.py +166 -0
- volknode/src/http_client/ai_suite_client/models/retry_ai_response_response_server_event_event_type.py +11 -0
- volknode/src/http_client/ai_suite_client/models/stop_indefinite_converse_answer_for_prompt_request.py +76 -0
- volknode/src/http_client/ai_suite_client/models/successful_login_response.py +84 -0
- volknode/src/http_client/ai_suite_client/models/user_created_successfully.py +108 -0
- volknode/src/http_client/ai_suite_client/models/user_resource_operations.py +84 -0
- volknode/src/http_client/ai_suite_client/models/user_role.py +88 -0
- volknode/src/http_client/ai_suite_client/models/user_role_name.py +8 -0
- volknode/src/http_client/ai_suite_client/models/view_llm_nodes_response.py +108 -0
- volknode/src/http_client/ai_suite_client/models/view_one_llm_node.py +100 -0
- volknode/src/http_client/ai_suite_client/models/view_one_user.py +132 -0
- volknode/src/http_client/ai_suite_client/models/view_users_response.py +108 -0
- volknode/src/http_client/ai_suite_client/py.typed +1 -0
- volknode/src/http_client/ai_suite_client/types.py +53 -0
- volknode/src/http_client/pyproject.toml +26 -0
- volknode/src/llm/__init__.py +16 -0
- volknode/src/llm/allowed_models.py +35 -0
- volknode/src/llm/fake_llm_runner.py +32 -0
- volknode/src/llm/fake_vllm_server.py +154 -0
- volknode/src/llm/llm_integration_test.py +331 -0
- volknode/src/llm/llm_runner.py +15 -0
- volknode/src/llm/pulled_model_info.py +9 -0
- volknode/src/llm/real_llm_runner.py +84 -0
- volknode/src/mtls/__init__.py +0 -0
- volknode/src/mtls/mtls_client.py +37 -0
- volknode/src/mtls/mtls_reverse_proxy.py +76 -0
- volknode/src/nexus/__init__.py +0 -0
- volknode/src/nexus/create_personal_access_token_test.py +77 -0
- volknode/src/nexus/fake_certificate_authority.py +94 -0
- volknode/src/nexus/fake_nexus.py +105 -0
- volknode/src/nexus/login_integration_test.py +67 -0
- volknode/src/nexus/nexus.py +22 -0
- volknode/src/nexus/real_nexus.py +48 -0
- volknode/src/nexus/register_llm_node_test.py +420 -0
- volknode/src/repository/__init__.py +7 -0
- volknode/src/repository/csr_credentials_repository.py +44 -0
- volknode/src/repository/llm_model_repository.py +70 -0
- volknode/src/repository/llm_node_certificate_repository.py +43 -0
- volknode/src/repository/nexus_certificate_repository.py +43 -0
- volknode/src/repository/running_vllm_config_repository.py +44 -0
- volknode/src/usecases/pull_model/__init__.py +0 -0
- volknode/src/usecases/pull_model/input.py +21 -0
- volknode/src/usecases/pull_model/interactor.py +20 -0
- volknode/src/usecases/pull_model/interactor_test.py +77 -0
- volknode/src/usecases/register_llm_node/__init__.py +0 -0
- volknode/src/usecases/register_llm_node/input.py +52 -0
- volknode/src/usecases/register_llm_node/interactor.py +62 -0
- volknode/src/usecases/register_llm_node/interactor_test.py +285 -0
- volknode/src/usecases/run_llm_node/__init__.py +0 -0
- volknode/src/usecases/run_llm_node/input.py +22 -0
- volknode/src/usecases/run_llm_node/interactor.py +33 -0
- volknode/src/usecases/run_llm_node/interactor_test.py +119 -0
- volknode/src/usecases/serve_llm/__init__.py +1 -0
- volknode/src/usecases/serve_llm/input.py +38 -0
- volknode/src/usecases/serve_llm/input_test.py +42 -0
- volknode/src/usecases/serve_llm/interactor.py +45 -0
- volknode/src/usecases/serve_llm/interactor_test.py +174 -0
- volknode-0.2.5.dist-info/METADATA +11 -0
- volknode-0.2.5.dist-info/RECORD +151 -0
- volknode-0.2.5.dist-info/WHEEL +4 -0
- volknode-0.2.5.dist-info/entry_points.txt +2 -0
volknode/__init__.py
ADDED
|
File without changes
|
volknode/main.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pwd
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from volknode.src.dependency_resolver import DependencyResolver
|
|
9
|
+
from volknode.src.repository import CsrCredentialsRepository, LlmModelRepository, LlmNodeCertificateRepository, NexusCertificateRepository, RunningVllmConfigRepository
|
|
10
|
+
from volknode.src.usecases.pull_model.input import PullModelInput
|
|
11
|
+
from volknode.src.usecases.pull_model.interactor import PullModelInteractor
|
|
12
|
+
from volknode.src.usecases.register_llm_node.input import RegisterLlmNodeInput
|
|
13
|
+
from volknode.src.usecases.register_llm_node.interactor import RegisterLlmNodeInteractor
|
|
14
|
+
from volknode.src.usecases.run_llm_node.input import RunLlmNodeInput
|
|
15
|
+
from volknode.src.usecases.run_llm_node.interactor import RunLlmNodeInteractor
|
|
16
|
+
from volknode.src.usecases.serve_llm.input import ServeLlmInput
|
|
17
|
+
from volknode.src.usecases.serve_llm.interactor import ServeLlmInteractor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_user_home() -> Path:
|
|
21
|
+
"""Get actual user's home directory, ignoring HOME env var."""
|
|
22
|
+
return Path(pwd.getpwuid(os.getuid()).pw_dir)
|
|
23
|
+
|
|
24
|
+
def _get_db_path() -> Path:
|
|
25
|
+
db_dir = get_user_home() / ".volknode"
|
|
26
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return db_dir / "volknode.db"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
app = typer.Typer()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def version_callback(value: bool):
|
|
34
|
+
if value:
|
|
35
|
+
print(f"volknode {version('volknode')}")
|
|
36
|
+
raise typer.Exit()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback()
|
|
40
|
+
def callback(
|
|
41
|
+
version: bool = typer.Option(
|
|
42
|
+
None, "--version", "-v", callback=version_callback, is_eager=True,
|
|
43
|
+
help="Show version and exit."
|
|
44
|
+
),
|
|
45
|
+
):
|
|
46
|
+
"""Volknode CLI application."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@app.command("pull")
|
|
50
|
+
def pull_command(
|
|
51
|
+
model_name: str = typer.Argument(..., help="The name of the model to pull."),
|
|
52
|
+
):
|
|
53
|
+
"""Pull a model from Hugging Face."""
|
|
54
|
+
user_home = get_user_home()
|
|
55
|
+
os.environ["HOME"] = str(user_home)
|
|
56
|
+
|
|
57
|
+
input_data, validation = PullModelInput.create(model_name)
|
|
58
|
+
if validation.has_errors():
|
|
59
|
+
for err in validation.errors():
|
|
60
|
+
print(f"Error: {err}")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
db_path = _get_db_path()
|
|
64
|
+
llm = DependencyResolver.from_env().llm_runner(cache_dir=user_home / ".cache")
|
|
65
|
+
llm_model_repo = LlmModelRepository(db_path)
|
|
66
|
+
|
|
67
|
+
interactor = PullModelInteractor(llm, llm_model_repo)
|
|
68
|
+
|
|
69
|
+
print(f"Pulling model: {model_name}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
interactor.execute(input_data)
|
|
73
|
+
print(f"Successfully pulled model: {model_name}")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Error pulling model: {e}")
|
|
76
|
+
raise typer.Exit(1)
|
|
77
|
+
|
|
78
|
+
@app.command("run")
|
|
79
|
+
def run_command(
|
|
80
|
+
model: str = typer.Option(..., help="Model to serve"),
|
|
81
|
+
):
|
|
82
|
+
"""Run vLLM server with the specified model."""
|
|
83
|
+
user_home = get_user_home()
|
|
84
|
+
os.environ["HOME"] = str(user_home)
|
|
85
|
+
|
|
86
|
+
input_data, validation = RunLlmNodeInput.create(model)
|
|
87
|
+
if validation.has_errors():
|
|
88
|
+
for err in validation.errors():
|
|
89
|
+
print(f"Error: {err}")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
db_path = _get_db_path()
|
|
93
|
+
llm = DependencyResolver.from_env().llm_runner(cache_dir=user_home / ".cache")
|
|
94
|
+
llm_model_repo = LlmModelRepository(db_path)
|
|
95
|
+
running_vllm_config_repo = RunningVllmConfigRepository(db_path)
|
|
96
|
+
|
|
97
|
+
interactor = RunLlmNodeInteractor(llm, llm_model_repo, running_vllm_config_repo)
|
|
98
|
+
|
|
99
|
+
print(f"Starting vLLM server with model: {model}")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
interactor.execute(input_data)
|
|
103
|
+
print("vLLM server is ready.")
|
|
104
|
+
import signal
|
|
105
|
+
signal.pause()
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
print("\nShutting down vLLM server...")
|
|
108
|
+
llm.stop()
|
|
109
|
+
except RuntimeError as e:
|
|
110
|
+
print(f"Error: {e}")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command("register")
|
|
115
|
+
def register_command(
|
|
116
|
+
nexus_url: str = typer.Option(..., "--nexus-url", help="The URL of the nexus server."),
|
|
117
|
+
url: str = typer.Option(..., "--url", help="The URL that the nexus can reach this LLM node with."),
|
|
118
|
+
personal_access_token: str = typer.Option(..., "--personal-access-token", help="Personal access token for authentication."),
|
|
119
|
+
llm_model_alias: str = typer.Option(..., "--llm-model-alias", help="The organization friendly alias for the LLM model."),
|
|
120
|
+
trust_insecure_remote_app_url: bool = typer.Option(False, "--trust-insecure-url", help="Skip TLS certificate verification for the nexus URL (e.g. self-signed certificates)."),
|
|
121
|
+
):
|
|
122
|
+
"""Register this LLM node with the nexus."""
|
|
123
|
+
input_data, validation = RegisterLlmNodeInput.create(url, personal_access_token, llm_model_alias)
|
|
124
|
+
|
|
125
|
+
if validation.has_errors():
|
|
126
|
+
for err in validation.errors():
|
|
127
|
+
print(f"Error: {err}")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
db_path = _get_db_path()
|
|
132
|
+
nexus = DependencyResolver.from_env().nexus(nexus_url=nexus_url, verify_ssl=not trust_insecure_remote_app_url)
|
|
133
|
+
llm_model_repo = LlmModelRepository(db_path)
|
|
134
|
+
csr_credentials_repo = CsrCredentialsRepository(db_path)
|
|
135
|
+
llm_node_certificate_repo = LlmNodeCertificateRepository(db_path)
|
|
136
|
+
nexus_certificate_repo = NexusCertificateRepository(db_path)
|
|
137
|
+
|
|
138
|
+
interactor = RegisterLlmNodeInteractor(
|
|
139
|
+
nexus=nexus,
|
|
140
|
+
llm_model_repository=llm_model_repo,
|
|
141
|
+
csr_credentials_repository=csr_credentials_repo,
|
|
142
|
+
llm_node_certificate_repository=llm_node_certificate_repo,
|
|
143
|
+
nexus_certificate_repository=nexus_certificate_repo,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
print(f"Registering LLM node with model alias '{input_data.llm_model_alias}' with nexus at {nexus_url}")
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
interactor.execute(input_data)
|
|
150
|
+
print("Registration successful.")
|
|
151
|
+
except RuntimeError as e:
|
|
152
|
+
print(f"Error: {e}")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command("serve")
|
|
157
|
+
def serve_command(
|
|
158
|
+
port: int = typer.Option(..., "--port", help="Port for the secure server that will serve LLM requests."),
|
|
159
|
+
with_test_cert_pem: str = typer.Option(None, "--with-test-cert-pem", hidden=True),
|
|
160
|
+
with_test_priv_key_pem: str = typer.Option(None, "--with-test-priv-key-pem", hidden=True),
|
|
161
|
+
with_test_ca_cert_pem: str = typer.Option(None, "--with-test-ca-cert-pem", hidden=True),
|
|
162
|
+
):
|
|
163
|
+
"""Start the secure server to expose the LLM to the nexus."""
|
|
164
|
+
from volknode.src.mtls.mtls_reverse_proxy import MtlsReverseProxy
|
|
165
|
+
|
|
166
|
+
input_data, validation = ServeLlmInput.create(
|
|
167
|
+
port,
|
|
168
|
+
test_cert_pem=with_test_cert_pem,
|
|
169
|
+
test_priv_key_pem=with_test_priv_key_pem,
|
|
170
|
+
test_ca_cert_pem=with_test_ca_cert_pem,
|
|
171
|
+
)
|
|
172
|
+
if validation.has_errors():
|
|
173
|
+
for err in validation.errors():
|
|
174
|
+
print(f"Error: {err}")
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
db_path = _get_db_path()
|
|
178
|
+
csr_credentials_repo = CsrCredentialsRepository(db_path)
|
|
179
|
+
llm_node_certificate_repo = LlmNodeCertificateRepository(db_path)
|
|
180
|
+
nexus_certificate_repo = NexusCertificateRepository(db_path)
|
|
181
|
+
|
|
182
|
+
if input_data.test_cert_pem is not None:
|
|
183
|
+
from volknode.src.domain.csr_credentials import CsrCredentials
|
|
184
|
+
from volknode.src.domain.llm_node_certificate import LlmNodeCertificate
|
|
185
|
+
from volknode.src.domain.nexus_certificate import NexusCertificate
|
|
186
|
+
|
|
187
|
+
csr_credentials_repo.save(CsrCredentials(private_key_pem=input_data.test_priv_key_pem, csr_pem=""))
|
|
188
|
+
llm_node_certificate_repo.save(LlmNodeCertificate(certificate_pem=input_data.test_cert_pem))
|
|
189
|
+
nexus_certificate_repo.save(NexusCertificate(certificate_pem=input_data.test_ca_cert_pem))
|
|
190
|
+
|
|
191
|
+
interactor = ServeLlmInteractor(
|
|
192
|
+
csr_credentials_repository=csr_credentials_repo,
|
|
193
|
+
llm_node_certificate_repository=llm_node_certificate_repo,
|
|
194
|
+
nexus_certificate_repository=nexus_certificate_repo,
|
|
195
|
+
running_vllm_config_repository=RunningVllmConfigRepository(db_path),
|
|
196
|
+
mtls_reverse_proxy=MtlsReverseProxy(),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
print(f"Starting secure server on port {port}")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
interactor.execute(input_data)
|
|
203
|
+
print("Secure server is running.")
|
|
204
|
+
import signal
|
|
205
|
+
signal.pause()
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
print("\nShutting down secure server...")
|
|
208
|
+
except RuntimeError as e:
|
|
209
|
+
print(f"Error: {e}")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TypeVar, Generic
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Result(Generic[T]):
|
|
9
|
+
value: T | None = None
|
|
10
|
+
error: str | None = None
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def is_ok(self) -> bool:
|
|
14
|
+
return self.error is None
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_error(self) -> bool:
|
|
18
|
+
return self.error is not None
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def ok(value: T) -> "Result[T]":
|
|
22
|
+
return Result(value=value)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def err(error: str) -> "Result[T]":
|
|
26
|
+
return Result(error=error)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Validation:
|
|
2
|
+
def __init__(self):
|
|
3
|
+
self._error_list: list[str] = []
|
|
4
|
+
|
|
5
|
+
def add_error(self, err: str) -> None:
|
|
6
|
+
self._error_list.append(err)
|
|
7
|
+
|
|
8
|
+
def merge_with(self, other: "Validation") -> None:
|
|
9
|
+
self._error_list.extend(other._error_list)
|
|
10
|
+
|
|
11
|
+
def errors(self) -> list[str]:
|
|
12
|
+
return self._error_list
|
|
13
|
+
|
|
14
|
+
def error(self) -> str | None:
|
|
15
|
+
if not self._error_list:
|
|
16
|
+
return None
|
|
17
|
+
return "\n".join(self._error_list)
|
|
18
|
+
|
|
19
|
+
def has_errors(self) -> bool:
|
|
20
|
+
return len(self._error_list) > 0
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cryptography import x509
|
|
4
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
5
|
+
from cryptography.x509 import ObjectIdentifier
|
|
6
|
+
|
|
7
|
+
from volknode.src.csr.key_pair_generator import PrivateKey
|
|
8
|
+
|
|
9
|
+
_OID_LLM_MODEL_ALIAS = ObjectIdentifier("1.3.6.1.4.1.99999.1.1")
|
|
10
|
+
_OID_LLM_NODE_MODEL_NAME = ObjectIdentifier("1.3.6.1.4.1.99999.1.2")
|
|
11
|
+
_OID_LLM_NODE_MODEL_QUANTIZATION = ObjectIdentifier("1.3.6.1.4.1.99999.1.4")
|
|
12
|
+
_OID_LLM_NODE_MODEL_CONTEXT_WINDOW = ObjectIdentifier("1.3.6.1.4.1.99999.1.5")
|
|
13
|
+
_OID_LLM_NODE_LLM_NODE_URL = ObjectIdentifier("1.3.6.1.4.1.99999.1.10")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LlmNodeCsr:
|
|
18
|
+
model_alias: str
|
|
19
|
+
model_name: str
|
|
20
|
+
model_quantization: str
|
|
21
|
+
model_context_window: int
|
|
22
|
+
llm_node_url: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CsrService:
|
|
26
|
+
def generate_csr(self, private_key: PrivateKey, llm_node_csr: LlmNodeCsr) -> str:
|
|
27
|
+
"""Returns PEM-encoded CSR"""
|
|
28
|
+
rsa_private_key = serialization.load_pem_private_key(
|
|
29
|
+
private_key.pem.encode("utf-8"),
|
|
30
|
+
password=None,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
subject = x509.Name([
|
|
34
|
+
x509.NameAttribute(_OID_LLM_MODEL_ALIAS, llm_node_csr.model_alias),
|
|
35
|
+
x509.NameAttribute(_OID_LLM_NODE_MODEL_NAME, llm_node_csr.model_name),
|
|
36
|
+
x509.NameAttribute(_OID_LLM_NODE_MODEL_QUANTIZATION, llm_node_csr.model_quantization),
|
|
37
|
+
x509.NameAttribute(_OID_LLM_NODE_MODEL_CONTEXT_WINDOW, str(llm_node_csr.model_context_window)),
|
|
38
|
+
x509.NameAttribute(_OID_LLM_NODE_LLM_NODE_URL, llm_node_csr.llm_node_url),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
csr = (
|
|
42
|
+
x509.CertificateSigningRequestBuilder()
|
|
43
|
+
.subject_name(subject)
|
|
44
|
+
.sign(rsa_private_key, hashes.SHA256())
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return csr.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives import serialization
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PublicKey:
|
|
6
|
+
def __init__(self, pem: str):
|
|
7
|
+
self.pem = pem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrivateKey:
|
|
11
|
+
def __init__(self, pem: str, public_key: PublicKey):
|
|
12
|
+
self.pem = pem
|
|
13
|
+
self.public_key = public_key
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KeyPairGenerator:
|
|
17
|
+
def generate(self) -> PrivateKey:
|
|
18
|
+
private_key = rsa.generate_private_key(
|
|
19
|
+
public_exponent=65537,
|
|
20
|
+
key_size=2048,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
private_key_pem = private_key.private_bytes(
|
|
24
|
+
encoding=serialization.Encoding.PEM,
|
|
25
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
26
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
27
|
+
).decode("utf-8")
|
|
28
|
+
|
|
29
|
+
public_key_pem = private_key.public_key().public_bytes(
|
|
30
|
+
encoding=serialization.Encoding.PEM,
|
|
31
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
32
|
+
).decode("utf-8")
|
|
33
|
+
|
|
34
|
+
return PrivateKey(private_key_pem, PublicKey(public_key_pem))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from volknode.src.llm.llm_runner import LlmRunner
|
|
5
|
+
from volknode.src.nexus.nexus import Nexus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DependencyResolver:
|
|
9
|
+
def __init__(self, *, use_fake_llm: bool = False, use_fake_nexus: bool = False):
|
|
10
|
+
self._use_fake_llm = use_fake_llm
|
|
11
|
+
self._use_fake_nexus = use_fake_nexus
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def from_env() -> "DependencyResolver":
|
|
15
|
+
truthy = {"1", "true", "yes"}
|
|
16
|
+
return DependencyResolver(
|
|
17
|
+
use_fake_llm=os.environ.get("USE_FAKE_LLM", "").lower() in truthy,
|
|
18
|
+
use_fake_nexus=os.environ.get("USE_FAKE_NEXUS", "").lower() in truthy,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def llm_runner(self, cache_dir: Path) -> LlmRunner:
|
|
22
|
+
if self._use_fake_llm:
|
|
23
|
+
from volknode.src.llm.fake_llm_runner import FakeLlmRunner
|
|
24
|
+
return FakeLlmRunner()
|
|
25
|
+
from volknode.src.llm.real_llm_runner import RealLlmRunner
|
|
26
|
+
return RealLlmRunner(cache_dir)
|
|
27
|
+
|
|
28
|
+
def nexus(self, nexus_url: str, verify_ssl: bool = True) -> Nexus:
|
|
29
|
+
if self._use_fake_nexus:
|
|
30
|
+
from volknode.src.nexus.fake_nexus import FakeNexus
|
|
31
|
+
return FakeNexus()
|
|
32
|
+
from volknode.src.nexus.real_nexus import RealNexus
|
|
33
|
+
return RealNexus(nexus_url, verify_ssl=verify_ssl)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from volknode.src.dependency_resolver import DependencyResolver
|
|
5
|
+
from volknode.src.llm.fake_llm_runner import FakeLlmRunner
|
|
6
|
+
from volknode.src.llm.real_llm_runner import RealLlmRunner
|
|
7
|
+
from volknode.src.nexus.fake_nexus import FakeNexus
|
|
8
|
+
from volknode.src.nexus.real_nexus import RealNexus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestDependencyResolver:
|
|
12
|
+
def test_default_returns_real_llm_runner(self, tmp_path: Path):
|
|
13
|
+
resolver = DependencyResolver()
|
|
14
|
+
result = resolver.llm_runner(cache_dir=tmp_path)
|
|
15
|
+
assert isinstance(result, RealLlmRunner)
|
|
16
|
+
|
|
17
|
+
def test_default_returns_real_nexus(self):
|
|
18
|
+
resolver = DependencyResolver()
|
|
19
|
+
result = resolver.nexus(nexus_url="http://localhost:8080")
|
|
20
|
+
assert isinstance(result, RealNexus)
|
|
21
|
+
|
|
22
|
+
def test_use_fake_llm_returns_fake_llm_runner(self, tmp_path: Path):
|
|
23
|
+
resolver = DependencyResolver(use_fake_llm=True)
|
|
24
|
+
result = resolver.llm_runner(cache_dir=tmp_path)
|
|
25
|
+
assert isinstance(result, FakeLlmRunner)
|
|
26
|
+
|
|
27
|
+
def test_use_fake_nexus_returns_fake_nexus(self):
|
|
28
|
+
resolver = DependencyResolver(use_fake_nexus=True)
|
|
29
|
+
result = resolver.nexus(nexus_url="http://localhost:8080")
|
|
30
|
+
assert isinstance(result, FakeNexus)
|
|
31
|
+
|
|
32
|
+
def test_from_env_defaults_to_real_when_vars_unset(self, tmp_path: Path, monkeypatch):
|
|
33
|
+
monkeypatch.delenv("USE_FAKE_LLM", raising=False)
|
|
34
|
+
monkeypatch.delenv("USE_FAKE_NEXUS", raising=False)
|
|
35
|
+
resolver = DependencyResolver.from_env()
|
|
36
|
+
assert isinstance(resolver.llm_runner(cache_dir=tmp_path), RealLlmRunner)
|
|
37
|
+
assert isinstance(resolver.nexus(nexus_url="http://localhost:8080"), RealNexus)
|
|
38
|
+
|
|
39
|
+
def test_from_env_reads_use_fake_llm(self, tmp_path: Path, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("USE_FAKE_LLM", "true")
|
|
41
|
+
monkeypatch.delenv("USE_FAKE_NEXUS", raising=False)
|
|
42
|
+
resolver = DependencyResolver.from_env()
|
|
43
|
+
assert isinstance(resolver.llm_runner(cache_dir=tmp_path), FakeLlmRunner)
|
|
44
|
+
|
|
45
|
+
def test_from_env_reads_use_fake_nexus(self, monkeypatch):
|
|
46
|
+
monkeypatch.delenv("USE_FAKE_LLM", raising=False)
|
|
47
|
+
monkeypatch.setenv("USE_FAKE_NEXUS", "1")
|
|
48
|
+
resolver = DependencyResolver.from_env()
|
|
49
|
+
assert isinstance(resolver.nexus(nexus_url="http://localhost:8080"), FakeNexus)
|
|
50
|
+
|
|
51
|
+
def test_from_env_accepts_yes(self, tmp_path: Path, monkeypatch):
|
|
52
|
+
monkeypatch.setenv("USE_FAKE_LLM", "yes")
|
|
53
|
+
monkeypatch.setenv("USE_FAKE_NEXUS", "YES")
|
|
54
|
+
resolver = DependencyResolver.from_env()
|
|
55
|
+
assert isinstance(resolver.llm_runner(cache_dir=tmp_path), FakeLlmRunner)
|
|
56
|
+
assert isinstance(resolver.nexus(nexus_url="http://localhost:8080"), FakeNexus)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
build/
|
|
3
|
+
dist/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
|
|
7
|
+
# pyenv
|
|
8
|
+
.python-version
|
|
9
|
+
|
|
10
|
+
# Environments
|
|
11
|
+
.env
|
|
12
|
+
.venv
|
|
13
|
+
|
|
14
|
+
# mypy
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.dmypy.json
|
|
17
|
+
dmypy.json
|
|
18
|
+
|
|
19
|
+
# JetBrains
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
/coverage.xml
|
|
23
|
+
/.coverage
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# ai-suite-client
|
|
2
|
+
A client library for accessing Ai Suite
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
First, create a client:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from ai_suite_client import Client
|
|
9
|
+
|
|
10
|
+
client = Client(base_url="https://api.example.com")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from ai_suite_client import AuthenticatedClient
|
|
17
|
+
|
|
18
|
+
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Now call your endpoint and use your models:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from ai_suite_client.models import MyDataModel
|
|
25
|
+
from ai_suite_client.api.my_tag import get_my_data_model
|
|
26
|
+
from ai_suite_client.types import Response
|
|
27
|
+
|
|
28
|
+
with client as client:
|
|
29
|
+
my_data: MyDataModel = get_my_data_model.sync(client=client)
|
|
30
|
+
# or if you need more info (e.g. status_code)
|
|
31
|
+
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or do the same thing with an async version:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ai_suite_client.models import MyDataModel
|
|
38
|
+
from ai_suite_client.api.my_tag import get_my_data_model
|
|
39
|
+
from ai_suite_client.types import Response
|
|
40
|
+
|
|
41
|
+
async with client as client:
|
|
42
|
+
my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
|
|
43
|
+
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
client = AuthenticatedClient(
|
|
50
|
+
base_url="https://internal_api.example.com",
|
|
51
|
+
token="SuperSecretToken",
|
|
52
|
+
verify_ssl="/path/to/certificate_bundle.pem",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can also disable certificate validation altogether, but beware that **this is a security risk**.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = AuthenticatedClient(
|
|
60
|
+
base_url="https://internal_api.example.com",
|
|
61
|
+
token="SuperSecretToken",
|
|
62
|
+
verify_ssl=False
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Things to know:
|
|
67
|
+
1. Every path/method combo becomes a Python module with four functions:
|
|
68
|
+
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
|
|
69
|
+
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
|
|
70
|
+
1. `asyncio`: Like `sync` but async instead of blocking
|
|
71
|
+
1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking
|
|
72
|
+
|
|
73
|
+
1. All path/query params, and bodies become method arguments.
|
|
74
|
+
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
|
|
75
|
+
1. Any endpoint which did not have a tag will be in `ai_suite_client.api.default`
|
|
76
|
+
|
|
77
|
+
## Advanced customizations
|
|
78
|
+
|
|
79
|
+
There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case):
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from ai_suite_client import Client
|
|
83
|
+
|
|
84
|
+
def log_request(request):
|
|
85
|
+
print(f"Request event hook: {request.method} {request.url} - Waiting for response")
|
|
86
|
+
|
|
87
|
+
def log_response(response):
|
|
88
|
+
request = response.request
|
|
89
|
+
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
|
|
90
|
+
|
|
91
|
+
client = Client(
|
|
92
|
+
base_url="https://api.example.com",
|
|
93
|
+
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import httpx
|
|
103
|
+
from ai_suite_client import Client
|
|
104
|
+
|
|
105
|
+
client = Client(
|
|
106
|
+
base_url="https://api.example.com",
|
|
107
|
+
)
|
|
108
|
+
# Note that base_url needs to be re-set, as would any shared cookies, headers, etc.
|
|
109
|
+
client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030"))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Building / publishing this package
|
|
113
|
+
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
|
|
114
|
+
1. Update the metadata in pyproject.toml (e.g. authors, version)
|
|
115
|
+
1. If you're using a private repository, configure it with Poetry
|
|
116
|
+
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
|
|
117
|
+
1. `poetry config http-basic.<your-repository-name> <username> <password>`
|
|
118
|
+
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`
|
|
119
|
+
|
|
120
|
+
If you want to install this client into another project without publishing it (e.g. for development) then:
|
|
121
|
+
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
|
|
122
|
+
1. If that project is not using Poetry:
|
|
123
|
+
1. Build a wheel with `poetry build -f wheel`
|
|
124
|
+
1. Install that wheel from the other project `pip install <path-to-wheel>`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""" Contains methods for accessing the API """
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""" Contains endpoint functions for accessing the API """
|