gaard-llm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gaard_llm-0.1.0/PKG-INFO +28 -0
- gaard_llm-0.1.0/README.md +10 -0
- gaard_llm-0.1.0/pyproject.toml +31 -0
- gaard_llm-0.1.0/setup.cfg +4 -0
- gaard_llm-0.1.0/src/gaard_llm/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/anthropic/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/azure_openai/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/ollama/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/openai/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/openai_compatible/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/openai_compatible/client.py +74 -0
- gaard_llm-0.1.0/src/gaard_llm/providers/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm/providers/models.py +21 -0
- gaard_llm-0.1.0/src/gaard_llm/vertex/__init__.py +0 -0
- gaard_llm-0.1.0/src/gaard_llm.egg-info/PKG-INFO +28 -0
- gaard_llm-0.1.0/src/gaard_llm.egg-info/SOURCES.txt +18 -0
- gaard_llm-0.1.0/src/gaard_llm.egg-info/dependency_links.txt +1 -0
- gaard_llm-0.1.0/src/gaard_llm.egg-info/requires.txt +14 -0
- gaard_llm-0.1.0/src/gaard_llm.egg-info/top_level.txt +1 -0
- gaard_llm-0.1.0/tests/test_openai_compatible_client.py +177 -0
gaard_llm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gaard-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM provider adapters for GAARD
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: gaard-core==0.1.0
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic>=2.7.0
|
|
10
|
+
Provides-Extra: openai
|
|
11
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
12
|
+
Provides-Extra: anthropic
|
|
13
|
+
Requires-Dist: anthropic>=0.34.0; extra == "anthropic"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
17
|
+
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# GAARD - Governed AI Access to Relational Data
|
|
20
|
+
|
|
21
|
+
GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
|
|
22
|
+
|
|
23
|
+
GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
|
|
24
|
+
|
|
25
|
+
For more informacion see https://github.com/pkroliszewski/gaard
|
|
26
|
+
|
|
27
|
+
# This package
|
|
28
|
+
Package gaard-llm extends gaard functionality by adding support for various ai providers.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# GAARD - Governed AI Access to Relational Data
|
|
2
|
+
|
|
3
|
+
GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
|
|
4
|
+
|
|
5
|
+
GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
|
|
6
|
+
|
|
7
|
+
For more informacion see https://github.com/pkroliszewski/gaard
|
|
8
|
+
|
|
9
|
+
# This package
|
|
10
|
+
Package gaard-llm extends gaard functionality by adding support for various ai providers.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gaard-llm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LLM provider adapters for GAARD"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"gaard-core==0.1.0",
|
|
13
|
+
"httpx>=0.27.0",
|
|
14
|
+
"pydantic>=2.7.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
openai = ["openai>=1.0.0"]
|
|
19
|
+
anthropic = ["anthropic>=0.34.0"]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0.0",
|
|
22
|
+
"ruff>=0.5.0",
|
|
23
|
+
"mypy>=1.10.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.ruff]
|
|
27
|
+
line-length = 100
|
|
28
|
+
target-version = "py311"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from gaard_core.errors import LlmProviderError
|
|
6
|
+
from gaard_llm.providers.models import ChatCompletionRequest, ChatCompletionResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAICompatibleClient:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
base_url: str,
|
|
13
|
+
api_key: str,
|
|
14
|
+
timeout_seconds: int = 60,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.base_url = base_url.rstrip("/")
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.timeout_seconds = timeout_seconds
|
|
19
|
+
|
|
20
|
+
def create_chat_completion(
|
|
21
|
+
self,
|
|
22
|
+
request: ChatCompletionRequest,
|
|
23
|
+
) -> ChatCompletionResponse:
|
|
24
|
+
url = f"{self.base_url}/chat/completions"
|
|
25
|
+
|
|
26
|
+
payload: dict[str, Any] = {
|
|
27
|
+
"model": request.model,
|
|
28
|
+
"messages": [
|
|
29
|
+
{
|
|
30
|
+
"role": message.role,
|
|
31
|
+
"content": message.content,
|
|
32
|
+
}
|
|
33
|
+
for message in request.messages
|
|
34
|
+
],
|
|
35
|
+
"temperature": request.temperature,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
payload.update(request.extra_body)
|
|
39
|
+
|
|
40
|
+
headers = {
|
|
41
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
response = httpx.post(
|
|
47
|
+
url,
|
|
48
|
+
json=payload,
|
|
49
|
+
headers=headers,
|
|
50
|
+
timeout=self.timeout_seconds,
|
|
51
|
+
)
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
except httpx.HTTPStatusError as exc:
|
|
54
|
+
detail = exc.response.text.strip()
|
|
55
|
+
detail_suffix = f" {detail[:500]}" if detail else ""
|
|
56
|
+
|
|
57
|
+
raise LlmProviderError(
|
|
58
|
+
f"LLM provider returned HTTP {exc.response.status_code}.{detail_suffix}"
|
|
59
|
+
) from exc
|
|
60
|
+
except httpx.HTTPError as exc:
|
|
61
|
+
raise LlmProviderError("LLM provider request failed.") from exc
|
|
62
|
+
|
|
63
|
+
data: dict[str, Any] = response.json()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
content = data["choices"][0]["message"]["content"]
|
|
67
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
68
|
+
raise LlmProviderError("Invalid OpenAI-compatible response format.") from exc
|
|
69
|
+
|
|
70
|
+
return ChatCompletionResponse(
|
|
71
|
+
content=content.strip(),
|
|
72
|
+
model=data.get("model"),
|
|
73
|
+
raw=data,
|
|
74
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChatMessage(BaseModel):
|
|
7
|
+
role: str
|
|
8
|
+
content: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChatCompletionRequest(BaseModel):
|
|
12
|
+
model: str
|
|
13
|
+
messages: list[ChatMessage]
|
|
14
|
+
temperature: float = 0.0
|
|
15
|
+
extra_body: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChatCompletionResponse(BaseModel):
|
|
19
|
+
content: str
|
|
20
|
+
model: str | None = None
|
|
21
|
+
raw: dict[str, Any] = Field(default_factory=dict)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gaard-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM provider adapters for GAARD
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: gaard-core==0.1.0
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic>=2.7.0
|
|
10
|
+
Provides-Extra: openai
|
|
11
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
12
|
+
Provides-Extra: anthropic
|
|
13
|
+
Requires-Dist: anthropic>=0.34.0; extra == "anthropic"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
17
|
+
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# GAARD - Governed AI Access to Relational Data
|
|
20
|
+
|
|
21
|
+
GAARD is a self-hosted AI SQL Gateway for governed natural-language access to relational data.
|
|
22
|
+
|
|
23
|
+
GAARD allows applications and users to ask questions about relational databases using natural language while keeping SQL generation, validation, execution, prompts, connectors, and auditability under control.
|
|
24
|
+
|
|
25
|
+
For more informacion see https://github.com/pkroliszewski/gaard
|
|
26
|
+
|
|
27
|
+
# This package
|
|
28
|
+
Package gaard-llm extends gaard functionality by adding support for various ai providers.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gaard_llm/__init__.py
|
|
4
|
+
src/gaard_llm.egg-info/PKG-INFO
|
|
5
|
+
src/gaard_llm.egg-info/SOURCES.txt
|
|
6
|
+
src/gaard_llm.egg-info/dependency_links.txt
|
|
7
|
+
src/gaard_llm.egg-info/requires.txt
|
|
8
|
+
src/gaard_llm.egg-info/top_level.txt
|
|
9
|
+
src/gaard_llm/anthropic/__init__.py
|
|
10
|
+
src/gaard_llm/azure_openai/__init__.py
|
|
11
|
+
src/gaard_llm/ollama/__init__.py
|
|
12
|
+
src/gaard_llm/openai/__init__.py
|
|
13
|
+
src/gaard_llm/openai_compatible/__init__.py
|
|
14
|
+
src/gaard_llm/openai_compatible/client.py
|
|
15
|
+
src/gaard_llm/providers/__init__.py
|
|
16
|
+
src/gaard_llm/providers/models.py
|
|
17
|
+
src/gaard_llm/vertex/__init__.py
|
|
18
|
+
tests/test_openai_compatible_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gaard_llm
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from gaard_core.errors import LlmProviderError
|
|
5
|
+
from gaard_llm.openai_compatible.client import OpenAICompatibleClient
|
|
6
|
+
from gaard_llm.providers.models import ChatCompletionRequest, ChatMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_openai_compatible_client_parses_response(monkeypatch) -> None:
|
|
10
|
+
captured_kwargs = {}
|
|
11
|
+
|
|
12
|
+
def fake_post(*args, **kwargs):
|
|
13
|
+
captured_kwargs.update(kwargs)
|
|
14
|
+
request = httpx.Request(
|
|
15
|
+
method="POST",
|
|
16
|
+
url="https://example.com/v1/chat/completions",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return httpx.Response(
|
|
20
|
+
status_code=200,
|
|
21
|
+
request=request,
|
|
22
|
+
json={
|
|
23
|
+
"model": "test-model",
|
|
24
|
+
"choices": [
|
|
25
|
+
{
|
|
26
|
+
"message": {
|
|
27
|
+
"content": "SELECT 1 AS value",
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(httpx, "post", fake_post)
|
|
35
|
+
|
|
36
|
+
client = OpenAICompatibleClient(
|
|
37
|
+
base_url="https://example.com/v1",
|
|
38
|
+
api_key="test-key",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
response = client.create_chat_completion(
|
|
42
|
+
ChatCompletionRequest(
|
|
43
|
+
model="test-model",
|
|
44
|
+
messages=[
|
|
45
|
+
ChatMessage(role="system", content="system"),
|
|
46
|
+
ChatMessage(role="user", content="user"),
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert response.content == "SELECT 1 AS value"
|
|
52
|
+
assert response.model == "test-model"
|
|
53
|
+
assert "chat_template_kwargs" not in captured_kwargs["json"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_openai_compatible_client_merges_extra_body(monkeypatch) -> None:
|
|
57
|
+
captured_kwargs = {}
|
|
58
|
+
|
|
59
|
+
def fake_post(*args, **kwargs):
|
|
60
|
+
captured_kwargs.update(kwargs)
|
|
61
|
+
request = httpx.Request(
|
|
62
|
+
method="POST",
|
|
63
|
+
url="https://example.com/v1/chat/completions",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return httpx.Response(
|
|
67
|
+
status_code=200,
|
|
68
|
+
request=request,
|
|
69
|
+
json={
|
|
70
|
+
"model": "test-model",
|
|
71
|
+
"choices": [
|
|
72
|
+
{
|
|
73
|
+
"message": {
|
|
74
|
+
"content": "SELECT 1 AS value",
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
monkeypatch.setattr(httpx, "post", fake_post)
|
|
82
|
+
|
|
83
|
+
client = OpenAICompatibleClient(
|
|
84
|
+
base_url="https://example.com/v1",
|
|
85
|
+
api_key="test-key",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
client.create_chat_completion(
|
|
89
|
+
ChatCompletionRequest(
|
|
90
|
+
model="test-model",
|
|
91
|
+
extra_body={
|
|
92
|
+
"chat_template_kwargs": {
|
|
93
|
+
"enable_thinking": False,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
messages=[
|
|
97
|
+
ChatMessage(role="system", content="system"),
|
|
98
|
+
ChatMessage(role="user", content="user"),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert captured_kwargs["json"]["chat_template_kwargs"] == {"enable_thinking": False}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_openai_compatible_client_wraps_invalid_response(monkeypatch) -> None:
|
|
107
|
+
def fake_post(*args, **kwargs):
|
|
108
|
+
request = httpx.Request(
|
|
109
|
+
method="POST",
|
|
110
|
+
url="https://example.com/v1/chat/completions",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return httpx.Response(
|
|
114
|
+
status_code=200,
|
|
115
|
+
request=request,
|
|
116
|
+
json={
|
|
117
|
+
"unexpected": "format",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
monkeypatch.setattr(httpx, "post", fake_post)
|
|
122
|
+
|
|
123
|
+
client = OpenAICompatibleClient(
|
|
124
|
+
base_url="https://example.com/v1",
|
|
125
|
+
api_key="test-key",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
with pytest.raises(LlmProviderError):
|
|
129
|
+
client.create_chat_completion(
|
|
130
|
+
ChatCompletionRequest(
|
|
131
|
+
model="test-model",
|
|
132
|
+
messages=[
|
|
133
|
+
ChatMessage(role="system", content="system"),
|
|
134
|
+
ChatMessage(role="user", content="user"),
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_openai_compatible_client_includes_provider_error_body(monkeypatch) -> None:
|
|
141
|
+
def fake_post(*args, **kwargs):
|
|
142
|
+
request = httpx.Request(
|
|
143
|
+
method="POST",
|
|
144
|
+
url="https://example.com/v1/chat/completions",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return httpx.Response(
|
|
148
|
+
status_code=400,
|
|
149
|
+
request=request,
|
|
150
|
+
json={
|
|
151
|
+
"error": {
|
|
152
|
+
"message": "Unrecognized request argument supplied.",
|
|
153
|
+
"type": "invalid_request_error",
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
monkeypatch.setattr(httpx, "post", fake_post)
|
|
159
|
+
|
|
160
|
+
client = OpenAICompatibleClient(
|
|
161
|
+
base_url="https://example.com/v1",
|
|
162
|
+
api_key="test-key",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
with pytest.raises(LlmProviderError) as exc_info:
|
|
166
|
+
client.create_chat_completion(
|
|
167
|
+
ChatCompletionRequest(
|
|
168
|
+
model="test-model",
|
|
169
|
+
messages=[
|
|
170
|
+
ChatMessage(role="system", content="system"),
|
|
171
|
+
ChatMessage(role="user", content="user"),
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert "LLM provider returned HTTP 400." in str(exc_info.value)
|
|
177
|
+
assert "Unrecognized request argument supplied." in str(exc_info.value)
|