locaith 1.0.0__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.
locaith/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ from ._client import AsyncLocaith, Locaith
2
+ from ._errors import (
3
+ APIConnectionError,
4
+ APIStatusError,
5
+ APITimeoutError,
6
+ AuthenticationError,
7
+ BadRequestError,
8
+ InternalServerError,
9
+ LocaithError,
10
+ NotFoundError,
11
+ RateLimitError,
12
+ )
13
+ from ._types import AsyncStream, ChatCompletion, ChatCompletionChunk, Stream
14
+ from ._version import __version__
15
+
16
+ __all__ = [
17
+ "Locaith",
18
+ "AsyncLocaith",
19
+ "ChatCompletion",
20
+ "ChatCompletionChunk",
21
+ "Stream",
22
+ "AsyncStream",
23
+ "LocaithError",
24
+ "APIConnectionError",
25
+ "APITimeoutError",
26
+ "APIStatusError",
27
+ "BadRequestError",
28
+ "AuthenticationError",
29
+ "NotFoundError",
30
+ "RateLimitError",
31
+ "InternalServerError",
32
+ "__version__",
33
+ ]
locaith/_client.py ADDED
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import time
6
+ from typing import Any, Dict, Mapping, Optional, Union
7
+
8
+ import httpx
9
+
10
+ from ._errors import APIConnectionError, APITimeoutError, LocaithError, make_status_error
11
+ from ._resources import AsyncChat, AsyncMemory, AsyncModels, Chat, Memory, Models
12
+ from ._version import __version__
13
+
14
+ DEFAULT_BASE_URL = "https://memory-api.locaith.com/v1"
15
+ DEFAULT_TIMEOUT = 900.0
16
+ DEFAULT_MAX_RETRIES = 2
17
+ _RETRY_STATUS = {502, 503, 504}
18
+
19
+
20
+ class _BaseClient:
21
+ def __init__(
22
+ self,
23
+ *,
24
+ api_key: Optional[str],
25
+ base_url: Optional[str],
26
+ timeout: Union[float, httpx.Timeout],
27
+ max_retries: int,
28
+ user: Optional[str],
29
+ default_headers: Optional[Mapping[str, str]],
30
+ ) -> None:
31
+ api_key = api_key or os.environ.get("LOCAITH_API_KEY")
32
+ if not api_key:
33
+ raise LocaithError(
34
+ "Thiếu API key. Truyền Locaith(api_key=...) hoặc đặt biến môi trường LOCAITH_API_KEY."
35
+ )
36
+ self.api_key = api_key
37
+ self.base_url = (base_url or os.environ.get("LOCAITH_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
38
+ self.timeout = timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout, connect=10.0)
39
+ self.max_retries = max_retries
40
+ self.user = user
41
+ self._headers: Dict[str, str] = {
42
+ "Authorization": f"Bearer {api_key}",
43
+ "Content-Type": "application/json",
44
+ "Accept": "application/json",
45
+ "User-Agent": f"locaith-python/{__version__}",
46
+ **(default_headers or {}),
47
+ }
48
+
49
+ def _url(self, path: str) -> str:
50
+ return self.base_url + path
51
+
52
+ @staticmethod
53
+ def _delay(attempt: int) -> float:
54
+ return min(0.5 * (2**attempt), 8.0)
55
+
56
+ @staticmethod
57
+ def _body(response: httpx.Response) -> Any:
58
+ try:
59
+ return response.json()
60
+ except ValueError:
61
+ return {"detail": response.text[:500]}
62
+
63
+
64
+ class Locaith(_BaseClient):
65
+ """Client đồng bộ.
66
+
67
+ from locaith import Locaith
68
+ client = Locaith() # đọc LOCAITH_API_KEY
69
+ r = client.chat.completions.create(messages=[{"role": "user", "content": "Xin chào"}])
70
+ print(r.choices[0].message.content)
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ api_key: Optional[str] = None,
76
+ *,
77
+ base_url: Optional[str] = None,
78
+ timeout: Union[float, httpx.Timeout] = DEFAULT_TIMEOUT,
79
+ max_retries: int = DEFAULT_MAX_RETRIES,
80
+ user: Optional[str] = None,
81
+ default_headers: Optional[Mapping[str, str]] = None,
82
+ http_client: Optional[httpx.Client] = None,
83
+ ) -> None:
84
+ super().__init__(api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries, user=user,
85
+ default_headers=default_headers)
86
+ self._http = http_client or httpx.Client(timeout=self.timeout)
87
+ self.chat = Chat(self)
88
+ self.models = Models(self)
89
+ self.memory = Memory(self)
90
+
91
+ def _send(self, method: str, path: str, *, json: Any = None, params: Optional[dict] = None,
92
+ stream: bool = False) -> httpx.Response:
93
+ req = self._http.build_request(method, self._url(path), json=json, params=params,
94
+ headers=self._headers, timeout=self.timeout)
95
+ attempt = 0
96
+ while True:
97
+ try:
98
+ resp = self._http.send(req, stream=stream)
99
+ except httpx.TimeoutException as e:
100
+ if attempt >= self.max_retries:
101
+ raise APITimeoutError("Quá thời gian chờ phản hồi.") from e
102
+ except httpx.TransportError as e:
103
+ if attempt >= self.max_retries:
104
+ raise APIConnectionError(f"Không kết nối được: {type(e).__name__}") from e
105
+ else:
106
+ if resp.status_code in _RETRY_STATUS and attempt < self.max_retries:
107
+ resp.close()
108
+ elif resp.status_code >= 400:
109
+ if stream:
110
+ resp.read()
111
+ err = make_status_error(resp.status_code, self._body(resp))
112
+ resp.close()
113
+ raise err
114
+ else:
115
+ return resp
116
+ time.sleep(self._delay(attempt))
117
+ attempt += 1
118
+
119
+ def close(self) -> None:
120
+ self._http.close()
121
+
122
+ def __enter__(self) -> "Locaith":
123
+ return self
124
+
125
+ def __exit__(self, *a: Any) -> None:
126
+ self.close()
127
+
128
+
129
+ class AsyncLocaith(_BaseClient):
130
+ """Client bất đồng bộ (asyncio), cùng giao diện với Locaith."""
131
+
132
+ def __init__(
133
+ self,
134
+ api_key: Optional[str] = None,
135
+ *,
136
+ base_url: Optional[str] = None,
137
+ timeout: Union[float, httpx.Timeout] = DEFAULT_TIMEOUT,
138
+ max_retries: int = DEFAULT_MAX_RETRIES,
139
+ user: Optional[str] = None,
140
+ default_headers: Optional[Mapping[str, str]] = None,
141
+ http_client: Optional[httpx.AsyncClient] = None,
142
+ ) -> None:
143
+ super().__init__(api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries, user=user,
144
+ default_headers=default_headers)
145
+ self._http = http_client or httpx.AsyncClient(timeout=self.timeout)
146
+ self.chat = AsyncChat(self)
147
+ self.models = AsyncModels(self)
148
+ self.memory = AsyncMemory(self)
149
+
150
+ async def _send(self, method: str, path: str, *, json: Any = None, params: Optional[dict] = None,
151
+ stream: bool = False) -> httpx.Response:
152
+ req = self._http.build_request(method, self._url(path), json=json, params=params,
153
+ headers=self._headers, timeout=self.timeout)
154
+ attempt = 0
155
+ while True:
156
+ try:
157
+ resp = await self._http.send(req, stream=stream)
158
+ except httpx.TimeoutException as e:
159
+ if attempt >= self.max_retries:
160
+ raise APITimeoutError("Quá thời gian chờ phản hồi.") from e
161
+ except httpx.TransportError as e:
162
+ if attempt >= self.max_retries:
163
+ raise APIConnectionError(f"Không kết nối được: {type(e).__name__}") from e
164
+ else:
165
+ if resp.status_code in _RETRY_STATUS and attempt < self.max_retries:
166
+ await resp.aclose()
167
+ elif resp.status_code >= 400:
168
+ if stream:
169
+ await resp.aread()
170
+ err = make_status_error(resp.status_code, self._body(resp))
171
+ await resp.aclose()
172
+ raise err
173
+ else:
174
+ return resp
175
+ await asyncio.sleep(self._delay(attempt))
176
+ attempt += 1
177
+
178
+ async def close(self) -> None:
179
+ await self._http.aclose()
180
+
181
+ async def __aenter__(self) -> "AsyncLocaith":
182
+ return self
183
+
184
+ async def __aexit__(self, *a: Any) -> None:
185
+ await self.close()
locaith/_errors.py ADDED
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class LocaithError(Exception):
7
+ """Lỗi gốc của SDK."""
8
+
9
+
10
+ class APIConnectionError(LocaithError):
11
+ """Không kết nối được tới máy chủ."""
12
+
13
+
14
+ class APITimeoutError(APIConnectionError):
15
+ """Quá thời gian chờ."""
16
+
17
+
18
+ class APIStatusError(LocaithError):
19
+ """Máy chủ trả mã lỗi HTTP."""
20
+
21
+ def __init__(self, message: str, *, status_code: int, body: Any = None) -> None:
22
+ super().__init__(message)
23
+ self.message = message
24
+ self.status_code = status_code
25
+ self.body = body
26
+
27
+ def __str__(self) -> str:
28
+ return f"Error code: {self.status_code} - {self.message}"
29
+
30
+
31
+ class BadRequestError(APIStatusError):
32
+ """400"""
33
+
34
+
35
+ class AuthenticationError(APIStatusError):
36
+ """401: sai hoặc thiếu API key"""
37
+
38
+
39
+ class NotFoundError(APIStatusError):
40
+ """404"""
41
+
42
+
43
+ class RateLimitError(APIStatusError):
44
+ """429"""
45
+
46
+
47
+ class InternalServerError(APIStatusError):
48
+ """5xx"""
49
+
50
+
51
+ def make_status_error(status: int, body: Any) -> APIStatusError:
52
+ msg: Optional[str] = None
53
+ if isinstance(body, dict):
54
+ err = body.get("error")
55
+ if isinstance(err, dict):
56
+ msg = err.get("message")
57
+ elif isinstance(body.get("detail"), str):
58
+ msg = body["detail"]
59
+ msg = msg or f"HTTP {status}"
60
+ cls = {400: BadRequestError, 401: AuthenticationError, 404: NotFoundError, 429: RateLimitError}.get(status)
61
+ if cls is None:
62
+ cls = InternalServerError if status >= 500 else APIStatusError
63
+ return cls(msg, status_code=status, body=body)
locaith/_resources.py ADDED
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
4
+
5
+ from ._types import AsyncStream, AttrDict, ChatCompletion, Stream
6
+
7
+ if TYPE_CHECKING:
8
+ from ._client import AsyncLocaith, Locaith
9
+
10
+ DEFAULT_MODEL = "Locaith-4-flash"
11
+ MEMORY_TYPES = ("user", "feedback", "project", "reference")
12
+
13
+
14
+ def _chat_body(c: Any, *, messages: Union[str, Iterable[Dict[str, Any]]], model: str, stream: bool,
15
+ web_search: bool, memory: bool, user: Optional[str], extra_body: Optional[Dict[str, Any]],
16
+ params: Dict[str, Any]) -> Dict[str, Any]:
17
+ if isinstance(messages, str):
18
+ messages = [{"role": "user", "content": messages}]
19
+ body: Dict[str, Any] = {"model": model, "messages": list(messages), "web_search": web_search, "memory": memory}
20
+ u = user or c.user
21
+ if u:
22
+ body["user"] = u
23
+ body.update({k: v for k, v in params.items() if v is not None})
24
+ if stream:
25
+ body["stream"] = True
26
+ if extra_body:
27
+ body.update(extra_body)
28
+ return body
29
+
30
+
31
+ def _qs(c: Any, user: Optional[str]) -> Dict[str, str]:
32
+ u = user or c.user
33
+ return {"user": u} if u else {}
34
+
35
+
36
+ def _check_type(type: str) -> None:
37
+ if type not in MEMORY_TYPES:
38
+ raise ValueError(f"type phải là một trong {MEMORY_TYPES}")
39
+
40
+
41
+ # ------------------------------------------------------------------ đồng bộ
42
+ class Completions:
43
+ def __init__(self, client: "Locaith") -> None:
44
+ self._c = client
45
+
46
+ def create(
47
+ self,
48
+ *,
49
+ messages: Union[str, Iterable[Dict[str, Any]]],
50
+ model: str = DEFAULT_MODEL,
51
+ stream: bool = False,
52
+ web_search: bool = True,
53
+ memory: bool = False,
54
+ user: Optional[str] = None,
55
+ temperature: Optional[float] = None,
56
+ top_p: Optional[float] = None,
57
+ max_tokens: Optional[int] = None,
58
+ stop: Union[str, List[str], None] = None,
59
+ seed: Optional[int] = None,
60
+ presence_penalty: Optional[float] = None,
61
+ frequency_penalty: Optional[float] = None,
62
+ tools: Optional[List[Dict[str, Any]]] = None,
63
+ tool_choice: Union[str, Dict[str, Any], None] = None,
64
+ response_format: Optional[Dict[str, Any]] = None,
65
+ reasoning_effort: Optional[str] = None,
66
+ extra_body: Optional[Dict[str, Any]] = None,
67
+ ) -> Union[ChatCompletion, Stream]:
68
+ """Tạo câu trả lời. Luôn suy luận. web_search=True (mặc định) tự tìm web;
69
+ memory=True dùng Bio-Memory (ký ức dài hạn + điểm lưu khi hết ngữ cảnh).
70
+ stream=True trả về Stream."""
71
+ params = dict(temperature=temperature, top_p=top_p, max_tokens=max_tokens, stop=stop, seed=seed,
72
+ presence_penalty=presence_penalty, frequency_penalty=frequency_penalty, tools=tools,
73
+ tool_choice=tool_choice, response_format=response_format, reasoning_effort=reasoning_effort)
74
+ body = _chat_body(self._c, messages=messages, model=model, stream=stream, web_search=web_search,
75
+ memory=memory, user=user, extra_body=extra_body, params=params)
76
+ if stream:
77
+ return Stream(self._c._send("POST", "/chat/completions", json=body, stream=True))
78
+ return ChatCompletion(self._c._send("POST", "/chat/completions", json=body).json())
79
+
80
+
81
+ class Chat:
82
+ def __init__(self, client: "Locaith") -> None:
83
+ self.completions = Completions(client)
84
+
85
+
86
+ class Models:
87
+ def __init__(self, client: "Locaith") -> None:
88
+ self._c = client
89
+
90
+ def list(self) -> List[AttrDict]:
91
+ return [AttrDict(m) for m in self._c._send("GET", "/models").json()["data"]]
92
+
93
+
94
+ class Memory:
95
+ """Bio-Memory: ký ức dài hạn, mỗi API key (và mỗi user) một kho riêng."""
96
+
97
+ def __init__(self, client: "Locaith") -> None:
98
+ self._c = client
99
+
100
+ def list(self, *, user: Optional[str] = None) -> List[AttrDict]:
101
+ return [AttrDict(m) for m in self._c._send("GET", "/memory", params=_qs(self._c, user)).json()["items"]]
102
+
103
+ def add(self, title: str, content: str, *, type: str = "project", user: Optional[str] = None) -> str:
104
+ """type: user | feedback | project | reference. Trả về 'đã lưu' / 'trùng' / 'bị từ chối'."""
105
+ _check_type(type)
106
+ body = {"title": title, "content": content, "type": type, "user": user or self._c.user}
107
+ return self._c._send("POST", "/memory", json=body).json()["result"]
108
+
109
+ def delete(self, memory_id: str, *, user: Optional[str] = None) -> None:
110
+ self._c._send("DELETE", f"/memory/{memory_id}", params=_qs(self._c, user))
111
+
112
+ def export(self, path: Optional[str] = None, *, format: str = "zip", user: Optional[str] = None) -> Any:
113
+ """Xuất ký ức ra file. format: zip (mỗi ký ức một .md + MEMORY.md) | md | json.
114
+ Có path thì ghi file và trả về path; không có thì trả về bytes (json trả về dict)."""
115
+ r = self._c._send("GET", "/memory/export", params={"format": format, **_qs(self._c, user)})
116
+ if format == "json" and path is None:
117
+ return r.json()
118
+ if path is None:
119
+ return r.content
120
+ with open(path, "wb") as f:
121
+ f.write(r.content)
122
+ return path
123
+
124
+
125
+ # ------------------------------------------------------------------ bất đồng bộ
126
+ class AsyncCompletions:
127
+ def __init__(self, client: "AsyncLocaith") -> None:
128
+ self._c = client
129
+
130
+ async def create(
131
+ self,
132
+ *,
133
+ messages: Union[str, Iterable[Dict[str, Any]]],
134
+ model: str = DEFAULT_MODEL,
135
+ stream: bool = False,
136
+ web_search: bool = True,
137
+ memory: bool = False,
138
+ user: Optional[str] = None,
139
+ temperature: Optional[float] = None,
140
+ top_p: Optional[float] = None,
141
+ max_tokens: Optional[int] = None,
142
+ stop: Union[str, List[str], None] = None,
143
+ seed: Optional[int] = None,
144
+ presence_penalty: Optional[float] = None,
145
+ frequency_penalty: Optional[float] = None,
146
+ tools: Optional[List[Dict[str, Any]]] = None,
147
+ tool_choice: Union[str, Dict[str, Any], None] = None,
148
+ response_format: Optional[Dict[str, Any]] = None,
149
+ reasoning_effort: Optional[str] = None,
150
+ extra_body: Optional[Dict[str, Any]] = None,
151
+ ) -> Union[ChatCompletion, AsyncStream]:
152
+ params = dict(temperature=temperature, top_p=top_p, max_tokens=max_tokens, stop=stop, seed=seed,
153
+ presence_penalty=presence_penalty, frequency_penalty=frequency_penalty, tools=tools,
154
+ tool_choice=tool_choice, response_format=response_format, reasoning_effort=reasoning_effort)
155
+ body = _chat_body(self._c, messages=messages, model=model, stream=stream, web_search=web_search,
156
+ memory=memory, user=user, extra_body=extra_body, params=params)
157
+ if stream:
158
+ return AsyncStream(await self._c._send("POST", "/chat/completions", json=body, stream=True))
159
+ return ChatCompletion((await self._c._send("POST", "/chat/completions", json=body)).json())
160
+
161
+
162
+ class AsyncChat:
163
+ def __init__(self, client: "AsyncLocaith") -> None:
164
+ self.completions = AsyncCompletions(client)
165
+
166
+
167
+ class AsyncModels:
168
+ def __init__(self, client: "AsyncLocaith") -> None:
169
+ self._c = client
170
+
171
+ async def list(self) -> List[AttrDict]:
172
+ return [AttrDict(m) for m in (await self._c._send("GET", "/models")).json()["data"]]
173
+
174
+
175
+ class AsyncMemory:
176
+ def __init__(self, client: "AsyncLocaith") -> None:
177
+ self._c = client
178
+
179
+ async def list(self, *, user: Optional[str] = None) -> List[AttrDict]:
180
+ r = await self._c._send("GET", "/memory", params=_qs(self._c, user))
181
+ return [AttrDict(m) for m in r.json()["items"]]
182
+
183
+ async def add(self, title: str, content: str, *, type: str = "project", user: Optional[str] = None) -> str:
184
+ _check_type(type)
185
+ body = {"title": title, "content": content, "type": type, "user": user or self._c.user}
186
+ return (await self._c._send("POST", "/memory", json=body)).json()["result"]
187
+
188
+ async def delete(self, memory_id: str, *, user: Optional[str] = None) -> None:
189
+ await self._c._send("DELETE", f"/memory/{memory_id}", params=_qs(self._c, user))
190
+
191
+ async def export(self, path: Optional[str] = None, *, format: str = "zip", user: Optional[str] = None) -> Any:
192
+ r = await self._c._send("GET", "/memory/export", params={"format": format, **_qs(self._c, user)})
193
+ if format == "json" and path is None:
194
+ return r.json()
195
+ if path is None:
196
+ return r.content
197
+ with open(path, "wb") as f:
198
+ f.write(r.content)
199
+ return path
locaith/_types.py ADDED
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
5
+
6
+ from ._errors import APIStatusError
7
+
8
+
9
+ def _wrap(v: Any) -> Any:
10
+ if isinstance(v, dict) and not isinstance(v, AttrDict):
11
+ return AttrDict(v)
12
+ if isinstance(v, list):
13
+ return [_wrap(i) for i in v]
14
+ return v
15
+
16
+
17
+ class AttrDict(dict):
18
+ """dict truy cập được bằng thuộc tính, giống đối tượng phản hồi của openai:
19
+ resp.choices[0].message.content. Trường không có trả về None."""
20
+
21
+ def __getattr__(self, name: str) -> Any:
22
+ if name.startswith("__"):
23
+ raise AttributeError(name)
24
+ return _wrap(self.get(name))
25
+
26
+ def __getitem__(self, key: str) -> Any:
27
+ return _wrap(super().__getitem__(key))
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ return dict(self)
31
+
32
+
33
+ class ChatCompletion(AttrDict):
34
+ """Phản hồi chat. Ngoài chuẩn OpenAI còn có .sources (nguồn web), .memory_hits, .checkpoint."""
35
+
36
+ @property
37
+ def text(self) -> str:
38
+ c = self.get("choices") or []
39
+ return ((c[0].get("message") or {}).get("content") or "") if c else ""
40
+
41
+ @property
42
+ def reasoning(self) -> str:
43
+ c = self.get("choices") or []
44
+ return ((c[0].get("message") or {}).get("reasoning") or "") if c else ""
45
+
46
+
47
+ class ChatCompletionChunk(AttrDict):
48
+ pass
49
+
50
+
51
+ class _StreamState:
52
+ def __init__(self) -> None:
53
+ self.sources: List[AttrDict] = []
54
+ self.memory_hits: int = 0
55
+ self.checkpoint: bool = False
56
+
57
+
58
+ def parse_sse_line(line: str, state: _StreamState) -> Optional[ChatCompletionChunk]:
59
+ line = line.strip()
60
+ if not line.startswith("data:"):
61
+ return None
62
+ data = line[5:].strip()
63
+ if not data or data == "[DONE]":
64
+ return None
65
+ d = json.loads(data)
66
+ if "error" in d and not d.get("choices"):
67
+ raise APIStatusError(str((d["error"] or {}).get("message", "stream error")), status_code=500, body=d)
68
+ if not d.get("choices"): # chunk đầu: nguồn web, số ký ức nhớ lại, điểm lưu
69
+ state.sources = [AttrDict(s) for s in d.get("sources") or []]
70
+ state.memory_hits = int(d.get("memory_hits") or 0)
71
+ state.checkpoint = bool(d.get("checkpoint"))
72
+ return None
73
+ return ChatCompletionChunk(d)
74
+
75
+
76
+ class _StreamBase:
77
+ sources = property(lambda self: self._state.sources)
78
+ memory_hits = property(lambda self: self._state.memory_hits)
79
+ checkpoint = property(lambda self: self._state.checkpoint)
80
+
81
+
82
+ class Stream(_StreamBase):
83
+ """Luồng chunk. for chunk in stream: ... hoặc for t in stream.text(): ...
84
+ Sau chunk đầu, stream.sources / .memory_hits / .checkpoint có giá trị."""
85
+
86
+ def __init__(self, response: Any) -> None:
87
+ self._r = response
88
+ self._state = _StreamState()
89
+
90
+ def __iter__(self) -> Iterator[ChatCompletionChunk]:
91
+ try:
92
+ for line in self._r.iter_lines():
93
+ ch = parse_sse_line(line, self._state)
94
+ if ch is not None:
95
+ yield ch
96
+ finally:
97
+ self.close()
98
+
99
+ def text(self) -> Iterator[str]:
100
+ """Chỉ các đoạn chữ của câu trả lời."""
101
+ for ch in self:
102
+ t = ch.choices[0].delta.content
103
+ if t:
104
+ yield t
105
+
106
+ def close(self) -> None:
107
+ self._r.close()
108
+
109
+ def __enter__(self) -> "Stream":
110
+ return self
111
+
112
+ def __exit__(self, *a: Any) -> None:
113
+ self.close()
114
+
115
+
116
+ class AsyncStream(_StreamBase):
117
+ def __init__(self, response: Any) -> None:
118
+ self._r = response
119
+ self._state = _StreamState()
120
+
121
+ async def __aiter__(self) -> AsyncIterator[ChatCompletionChunk]:
122
+ try:
123
+ async for line in self._r.aiter_lines():
124
+ ch = parse_sse_line(line, self._state)
125
+ if ch is not None:
126
+ yield ch
127
+ finally:
128
+ await self.close()
129
+
130
+ async def text(self) -> AsyncIterator[str]:
131
+ async for ch in self:
132
+ t = ch.choices[0].delta.content
133
+ if t:
134
+ yield t
135
+
136
+ async def close(self) -> None:
137
+ await self._r.aclose()
138
+
139
+ async def __aenter__(self) -> "AsyncStream":
140
+ return self
141
+
142
+ async def __aexit__(self, *a: Any) -> None:
143
+ await self.close()
locaith/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
locaith/py.typed ADDED
File without changes
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: locaith
3
+ Version: 1.0.0
4
+ Summary: Python SDK cho Locaith-4-flash: tìm web, suy luận, ngữ cảnh 64K và Bio-Memory.
5
+ Project-URL: Homepage, https://locaith.com
6
+ Author: Locaith
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai,llm,locaith,openai-compatible,sdk,vietnamese
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: Vietnamese
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: httpx<1,>=0.24
19
+ Description-Content-Type: text/markdown
20
+
21
+ # locaith
22
+
23
+ Python SDK cho **Locaith-4-flash**: tìm web, suy luận, ngữ cảnh 64K, Bio-Memory.
24
+
25
+ ```bash
26
+ pip install locaith
27
+ export LOCAITH_API_KEY="LocAI..."
28
+ ```
29
+
30
+ ```python
31
+ from locaith import Locaith
32
+
33
+ client = Locaith()
34
+
35
+ r = client.chat.completions.create(
36
+ messages=[{"role": "user", "content": "Giá vàng SJC hôm nay?"}],
37
+ )
38
+ print(r.choices[0].message.content)
39
+ print([s.url for s in r.sources])
40
+ ```
41
+
42
+ Streaming:
43
+
44
+ ```python
45
+ stream = client.chat.completions.create(
46
+ messages=[{"role": "user", "content": "Viết bài 500 chữ về AI tại Việt Nam"}],
47
+ stream=True,
48
+ )
49
+ for text in stream.text():
50
+ print(text, end="", flush=True)
51
+ print(stream.sources)
52
+ ```
53
+
54
+ Bio-Memory:
55
+
56
+ ```python
57
+ client = Locaith(user="khach_123")
58
+
59
+ client.chat.completions.create(
60
+ messages=[{"role": "user", "content": "Tôi là kế toán trưởng công ty ABC."}],
61
+ memory=True,
62
+ )
63
+ client.chat.completions.create(
64
+ messages=[{"role": "user", "content": "Bạn nhớ tôi làm nghề gì?"}],
65
+ memory=True,
66
+ )
67
+
68
+ client.memory.list()
69
+ client.memory.add("Quy ước báo cáo", "Đơn vị triệu đồng.", type="feedback")
70
+ client.memory.export("ky-uc.zip")
71
+ ```
72
+
73
+ Async:
74
+
75
+ ```python
76
+ import asyncio
77
+ from locaith import AsyncLocaith
78
+
79
+ async def main():
80
+ async with AsyncLocaith() as client:
81
+ r = await client.chat.completions.create(messages="Xin chào")
82
+ print(r.text)
83
+
84
+ asyncio.run(main())
85
+ ```
86
+
87
+ Lỗi:
88
+
89
+ ```python
90
+ import locaith
91
+
92
+ try:
93
+ client.chat.completions.create(messages="Xin chào")
94
+ except locaith.AuthenticationError as e:
95
+ print(e.status_code, e.message)
96
+ except locaith.APIConnectionError:
97
+ ...
98
+ ```
99
+
100
+ Tương thích OpenAI: có thể dùng thẳng SDK `openai` với `base_url="https://memory-api.locaith.com/v1"`.
@@ -0,0 +1,11 @@
1
+ locaith/__init__.py,sha256=qqgykytuj6wQ-RRZLHZfpy5ZO9r7Q5PuSueip-SKR6E,734
2
+ locaith/_client.py,sha256=jqYIw9JoELaYKzUu7t-mejg1FVXXbG8owNI7mZwF6k0,7062
3
+ locaith/_errors.py,sha256=bHY68V-S292P0Ee5ghvGL1iAxvVM5Cc_tKqKEpzgD_U,1615
4
+ locaith/_resources.py,sha256=j4HltzIX1cJSn61c0vC4HD2Vd0QQ85K5fJ6mnAuPYNA,8715
5
+ locaith/_types.py,sha256=AQ2mJaPuYhcte15XHvkwBqu5ght-oI2ZqiOQ_QOzbug,4330
6
+ locaith/_version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
7
+ locaith/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ locaith-1.0.0.dist-info/METADATA,sha256=RiRIM7DQkv4oBFoHsoJI7hPiQkeNsZOe40356gTA2nM,2475
9
+ locaith-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ locaith-1.0.0.dist-info/licenses/LICENSE,sha256=z83f5q00e6uNKQaJaLX2ExOKzKSu8Wdfq_YUoQFileU,1064
11
+ locaith-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Locaith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.