amsdal_ml 0.1.0__py3-none-any.whl → 0.1.2__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.
- amsdal_ml/__about__.py +1 -1
- amsdal_ml/agents/agent.py +31 -7
- amsdal_ml/agents/default_qa_agent.py +20 -12
- amsdal_ml/agents/promts/react_chat.prompt +10 -1
- amsdal_ml/agents/retriever_tool.py +25 -6
- amsdal_ml/fileio/__init__.py +0 -0
- amsdal_ml/fileio/base_loader.py +63 -0
- amsdal_ml/fileio/openai_loader.py +69 -0
- amsdal_ml/mcp_client/stdio_client.py +22 -2
- amsdal_ml/mcp_server/server_retriever_stdio.py +49 -1
- amsdal_ml/migrations/0000_initial.py +36 -0
- amsdal_ml/ml_ingesting/default_ingesting.py +4 -4
- amsdal_ml/ml_models/models.py +50 -13
- amsdal_ml/ml_models/openai_model.py +251 -51
- amsdal_ml/ml_retrievers/default_retriever.py +5 -5
- amsdal_ml/models/embedding_model.py +2 -2
- {amsdal_ml-0.1.0.dist-info → amsdal_ml-0.1.2.dist-info}/METADATA +1 -1
- {amsdal_ml-0.1.0.dist-info → amsdal_ml-0.1.2.dist-info}/RECORD +19 -15
- {amsdal_ml-0.1.0.dist-info → amsdal_ml-0.1.2.dist-info}/WHEEL +0 -0
amsdal_ml/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.1.
|
|
1
|
+
__version__ = '0.1.2'
|
amsdal_ml/agents/agent.py
CHANGED
|
@@ -6,10 +6,13 @@ from collections.abc import AsyncIterator
|
|
|
6
6
|
from collections.abc import Iterator
|
|
7
7
|
from typing import Any
|
|
8
8
|
from typing import Literal
|
|
9
|
+
from typing import Optional
|
|
9
10
|
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
from pydantic import Field
|
|
12
13
|
|
|
14
|
+
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class AgentMessage(BaseModel):
|
|
15
18
|
role: Literal["SYSTEM", "USER", "ASSISTANT"]
|
|
@@ -24,16 +27,37 @@ class AgentOutput(BaseModel):
|
|
|
24
27
|
|
|
25
28
|
class Agent(ABC):
|
|
26
29
|
@abstractmethod
|
|
27
|
-
async def arun(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
async def arun(
|
|
31
|
+
self,
|
|
32
|
+
user_query: str,
|
|
33
|
+
*,
|
|
34
|
+
attachments: Optional[list[FileAttachment]] = None,
|
|
35
|
+
) -> AgentOutput:
|
|
36
|
+
...
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def astream(
|
|
40
|
+
self,
|
|
41
|
+
user_query: str,
|
|
42
|
+
*,
|
|
43
|
+
attachments: Optional[list[FileAttachment]] = None,
|
|
44
|
+
) -> AsyncIterator[str]:
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def run(
|
|
48
|
+
self,
|
|
49
|
+
user_query: str,
|
|
50
|
+
*,
|
|
51
|
+
attachments: Optional[list[FileAttachment]] = None,
|
|
52
|
+
) -> AgentOutput:
|
|
34
53
|
msg = "This agent is async-only. Use arun()."
|
|
35
54
|
raise NotImplementedError(msg)
|
|
36
55
|
|
|
37
|
-
def stream(
|
|
56
|
+
def stream(
|
|
57
|
+
self,
|
|
58
|
+
user_query: str,
|
|
59
|
+
*,
|
|
60
|
+
attachments: Optional[list[FileAttachment]] = None,
|
|
61
|
+
) -> Iterator[str]:
|
|
38
62
|
msg = "This agent is async-only. Use astream()."
|
|
39
63
|
raise NotImplementedError(msg)
|
|
@@ -13,6 +13,7 @@ from typing import no_type_check
|
|
|
13
13
|
from amsdal_ml.agents.agent import Agent
|
|
14
14
|
from amsdal_ml.agents.agent import AgentOutput
|
|
15
15
|
from amsdal_ml.agents.promts import get_prompt
|
|
16
|
+
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
16
17
|
from amsdal_ml.mcp_client.base import ToolClient
|
|
17
18
|
from amsdal_ml.mcp_client.base import ToolInfo
|
|
18
19
|
from amsdal_ml.ml_models.models import MLModel
|
|
@@ -30,6 +31,9 @@ _FINAL_RE = re.compile(
|
|
|
30
31
|
r"Final Answer:\s*(?P<answer>.+)",
|
|
31
32
|
re.DOTALL | re.IGNORECASE,
|
|
32
33
|
)
|
|
34
|
+
# ---------- constants ----------
|
|
35
|
+
|
|
36
|
+
_MAX_PARSE_RETRIES = 5
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
|
|
@@ -186,11 +190,11 @@ class DefaultQAAgent(Agent):
|
|
|
186
190
|
return str(content)
|
|
187
191
|
|
|
188
192
|
# ---------- core run ----------
|
|
189
|
-
def run(self,
|
|
193
|
+
def run(self, user_query: str, *, attachments: list[FileAttachment] | None = None) -> AgentOutput:
|
|
190
194
|
msg = "DefaultQAAgent is async-only for now. Use arun()."
|
|
191
195
|
raise NotImplementedError(msg)
|
|
192
196
|
|
|
193
|
-
async def _run_async(self, user_query: str) -> AgentOutput:
|
|
197
|
+
async def _run_async(self, user_query: str, *, attachments: list[FileAttachment] | None = None) -> AgentOutput:
|
|
194
198
|
if self._tool_clients and not self._tools_index_built:
|
|
195
199
|
await self._build_clients_index()
|
|
196
200
|
|
|
@@ -198,10 +202,12 @@ class DefaultQAAgent(Agent):
|
|
|
198
202
|
used_tools: list[str] = []
|
|
199
203
|
parse_retries = 0
|
|
200
204
|
|
|
205
|
+
|
|
201
206
|
for _ in range(self.max_steps):
|
|
202
207
|
prompt = self._react_text(user_query, scratch)
|
|
203
|
-
out = await self.model.ainvoke(prompt)
|
|
204
|
-
|
|
208
|
+
out = await self.model.ainvoke(prompt, attachments=attachments)
|
|
209
|
+
print("Model output:", out) # noqa: T201
|
|
210
|
+
print('promt:', prompt) # noqa: T201
|
|
205
211
|
m_final = _FINAL_RE.search(out or "")
|
|
206
212
|
if m_final:
|
|
207
213
|
return AgentOutput(
|
|
@@ -213,7 +219,7 @@ class DefaultQAAgent(Agent):
|
|
|
213
219
|
m_tool = _TOOL_CALL_RE.search(out or "")
|
|
214
220
|
if not m_tool:
|
|
215
221
|
parse_retries += 1
|
|
216
|
-
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >=
|
|
222
|
+
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >= _MAX_PARSE_RETRIES:
|
|
217
223
|
msg = (
|
|
218
224
|
"Invalid ReAct output. Expected EXACT format (Final or Tool-call). "
|
|
219
225
|
f"Got:\n{out}"
|
|
@@ -221,6 +227,7 @@ class DefaultQAAgent(Agent):
|
|
|
221
227
|
raise ValueError(
|
|
222
228
|
msg
|
|
223
229
|
)
|
|
230
|
+
|
|
224
231
|
scratch += (
|
|
225
232
|
"\nThought: Previous output violated the strict format. "
|
|
226
233
|
"Reply again using EXACTLY one of the two specified formats.\n"
|
|
@@ -237,7 +244,7 @@ class DefaultQAAgent(Agent):
|
|
|
237
244
|
raise ValueError(msg)
|
|
238
245
|
except Exception as e:
|
|
239
246
|
parse_retries += 1
|
|
240
|
-
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >=
|
|
247
|
+
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >= _MAX_PARSE_RETRIES:
|
|
241
248
|
msg = f"Invalid Action Input JSON: {raw_input!r} ({e})"
|
|
242
249
|
raise ValueError(msg) from e
|
|
243
250
|
scratch += (
|
|
@@ -250,6 +257,7 @@ class DefaultQAAgent(Agent):
|
|
|
250
257
|
|
|
251
258
|
try:
|
|
252
259
|
result = await tool.run(args, context=None, convert_result=True)
|
|
260
|
+
print("Similarity search result:", result) # noqa: T201
|
|
253
261
|
except Exception as e:
|
|
254
262
|
# unified error payload
|
|
255
263
|
err = {
|
|
@@ -276,12 +284,12 @@ class DefaultQAAgent(Agent):
|
|
|
276
284
|
return self._stopped_response(used_tools)
|
|
277
285
|
|
|
278
286
|
# ---------- public APIs ----------
|
|
279
|
-
async def arun(self, user_query: str) -> AgentOutput:
|
|
280
|
-
return await self._run_async(user_query)
|
|
287
|
+
async def arun(self, user_query: str, *, attachments: list[FileAttachment] | None = None) -> AgentOutput:
|
|
288
|
+
return await self._run_async(user_query, attachments=attachments)
|
|
281
289
|
|
|
282
290
|
# ---------- streaming ----------
|
|
283
291
|
@no_type_check
|
|
284
|
-
async def astream(self, user_query: str) -> AsyncIterator[str]:
|
|
292
|
+
async def astream(self, user_query: str, *, attachments: list[FileAttachment] | None = None) -> AsyncIterator[str]:
|
|
285
293
|
if self._tool_clients and not self._tools_index_built:
|
|
286
294
|
await self._build_clients_index()
|
|
287
295
|
|
|
@@ -296,7 +304,7 @@ class DefaultQAAgent(Agent):
|
|
|
296
304
|
|
|
297
305
|
# Normalize model.astream: it might be an async iterator already,
|
|
298
306
|
# or a coroutine (or nested coroutines) that resolves to one.
|
|
299
|
-
_val = self.model.astream(prompt)
|
|
307
|
+
_val = self.model.astream(prompt, attachments=attachments)
|
|
300
308
|
while inspect.iscoroutine(_val):
|
|
301
309
|
_val = await _val
|
|
302
310
|
|
|
@@ -320,7 +328,7 @@ class DefaultQAAgent(Agent):
|
|
|
320
328
|
m_tool = _TOOL_CALL_RE.search(buffer or "")
|
|
321
329
|
if not m_tool:
|
|
322
330
|
parse_retries += 1
|
|
323
|
-
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >=
|
|
331
|
+
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >= _MAX_PARSE_RETRIES:
|
|
324
332
|
msg = f"Invalid ReAct output (stream). Expected EXACT format. Got:\n{buffer}"
|
|
325
333
|
raise ValueError(msg)
|
|
326
334
|
scratch += (
|
|
@@ -339,7 +347,7 @@ class DefaultQAAgent(Agent):
|
|
|
339
347
|
raise ValueError(msg)
|
|
340
348
|
except Exception as e:
|
|
341
349
|
parse_retries += 1
|
|
342
|
-
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >=
|
|
350
|
+
if self.on_parse_error == ParseErrorMode.RAISE or parse_retries >= _MAX_PARSE_RETRIES:
|
|
343
351
|
msg = f"Invalid Action Input JSON: {raw_input!r} ({e})"
|
|
344
352
|
raise ValueError(msg) from e
|
|
345
353
|
scratch += (
|
|
@@ -21,7 +21,16 @@ RULES
|
|
|
21
21
|
- `Action Input` MUST be a valid ONE-LINE JSON object (e.g. {{"a": 1, "b": 2}}).
|
|
22
22
|
- Do NOT add anything before/after the block.
|
|
23
23
|
- Do NOT print "Observation". The system will add it after tool execution.
|
|
24
|
-
|
|
24
|
+
- You CAN read and use any attached files provided by the platform (including PDFs). Do not claim inability to view files.
|
|
25
|
+
- Prefer not to use tools when the answer is fully supported by the user’s message and/or attached files. Use a tool only if it materially improves accuracy or retrieves missing facts.
|
|
26
|
+
- If information is insufficient, answer concisely with: “I don’t have enough data to answer.”
|
|
27
|
+
- Be deterministic and concise. No preambles, no meta-commentary, no Markdown.
|
|
28
|
+
- Numbers: preserve exact figures, units, and percentages; do not round unless asked.
|
|
29
|
+
- If you choose a tool:
|
|
30
|
+
- Use exactly one tool per step.
|
|
31
|
+
- The Action Input MUST be a single-line valid JSON object that matches the tool’s schema.
|
|
32
|
+
- If a tool errors or returns nothing useful, switch to the other format and produce a Final Answer.
|
|
33
|
+
|
|
25
34
|
PREVIOUS CONVERSATION
|
|
26
35
|
--------------------
|
|
27
36
|
{chat_history}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
3
5
|
from typing import Any
|
|
4
6
|
from typing import Optional
|
|
5
7
|
|
|
@@ -9,6 +11,14 @@ from pydantic import Field
|
|
|
9
11
|
|
|
10
12
|
from amsdal_ml.ml_retrievers.openai_retriever import OpenAIRetriever
|
|
11
13
|
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
17
|
+
handlers=[
|
|
18
|
+
logging.FileHandler("server2.log"),
|
|
19
|
+
logging.StreamHandler(sys.stdout)
|
|
20
|
+
]
|
|
21
|
+
)
|
|
12
22
|
|
|
13
23
|
class RetrieverArgs(BaseModel):
|
|
14
24
|
query: str = Field(..., description='User search query')
|
|
@@ -20,13 +30,22 @@ class RetrieverArgs(BaseModel):
|
|
|
20
30
|
_retriever = OpenAIRetriever()
|
|
21
31
|
|
|
22
32
|
|
|
23
|
-
async def retriever_search(
|
|
33
|
+
async def retriever_search(
|
|
34
|
+
query: str = Field(..., description='User search query'),
|
|
35
|
+
k: int = 5,
|
|
36
|
+
include_tags: Optional[list[str]] = None,
|
|
37
|
+
exclude_tags: Optional[list[str]] = None,
|
|
38
|
+
) -> list[dict[str, Any]]:
|
|
39
|
+
logging.info(
|
|
40
|
+
f"retriever_search called with query={query}, k={k}, include_tags={include_tags}, exclude_tags={exclude_tags}"
|
|
41
|
+
)
|
|
24
42
|
chunks = await _retriever.asimilarity_search(
|
|
25
|
-
query=
|
|
26
|
-
k=
|
|
27
|
-
include_tags=
|
|
28
|
-
exclude_tags=
|
|
43
|
+
query=query,
|
|
44
|
+
k=k,
|
|
45
|
+
include_tags=include_tags,
|
|
46
|
+
exclude_tags=exclude_tags,
|
|
29
47
|
)
|
|
48
|
+
logging.info(f"retriever_search found {len(chunks)} chunks: {chunks}")
|
|
30
49
|
out: list[dict[str, Any]] = []
|
|
31
50
|
for c in chunks:
|
|
32
51
|
if hasattr(c, 'model_dump'):
|
|
@@ -42,7 +61,7 @@ async def retriever_search(args: RetrieverArgs) -> list[dict[str, Any]]:
|
|
|
42
61
|
|
|
43
62
|
retriever_tool = Tool.from_function(
|
|
44
63
|
retriever_search,
|
|
45
|
-
name='
|
|
64
|
+
name='search',
|
|
46
65
|
description='Semantic search in knowledge base (OpenAI embeddings)',
|
|
47
66
|
structured_output=True,
|
|
48
67
|
)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import BinaryIO
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
PLAIN_TEXT = "plain_text"
|
|
13
|
+
FILE_ID = "file_id"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileData(BaseModel):
|
|
17
|
+
name: str
|
|
18
|
+
size: int | None = None
|
|
19
|
+
mime: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FileAttachment:
|
|
24
|
+
type: str # one of: PLAIN_TEXT, FILE_ID
|
|
25
|
+
content: Any
|
|
26
|
+
metadata: dict[str, Any] | None = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
if self.metadata is None:
|
|
30
|
+
self.metadata = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FileItem:
|
|
35
|
+
file: BinaryIO
|
|
36
|
+
filename: str | None = None
|
|
37
|
+
filedata: FileData | None = None
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def from_path(path: str, *, filedata: FileData | None = None) -> FileItem:
|
|
41
|
+
# Caller is responsible for lifecycle; loaders may close after upload.
|
|
42
|
+
f = open(path, "rb")
|
|
43
|
+
return FileItem(file=f, filename=path.split("/")[-1], filedata=filedata)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def from_bytes(data: bytes, *, filename: str | None = None, filedata: FileData | None = None) -> FileItem:
|
|
47
|
+
import io
|
|
48
|
+
return FileItem(file=io.BytesIO(data), filename=filename, filedata=filedata)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def from_str(text: str, *, filename: str | None = None, filedata: FileData | None = None) -> FileItem:
|
|
52
|
+
import io
|
|
53
|
+
return FileItem(file=io.BytesIO(text.encode("utf-8")), filename=filename, filedata=filedata)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BaseFileLoader(ABC):
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def load(self, item: FileItem) -> FileAttachment:
|
|
59
|
+
"""Upload a single file and return an attachment reference."""
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def load_batch(self, items: list[FileItem]) -> list[FileAttachment]:
|
|
63
|
+
"""Upload multiple files; input and output are lists for simplicity."""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import io
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import BinaryIO
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from openai import AsyncOpenAI
|
|
12
|
+
|
|
13
|
+
from amsdal_ml.fileio.base_loader import FILE_ID
|
|
14
|
+
from amsdal_ml.fileio.base_loader import BaseFileLoader
|
|
15
|
+
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
16
|
+
from amsdal_ml.fileio.base_loader import FileData
|
|
17
|
+
from amsdal_ml.fileio.base_loader import FileItem
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
AllowedPurpose = Literal["assistants", "batch", "fine-tune", "vision", "user_data", "evals"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OpenAIFileLoader(BaseFileLoader):
|
|
25
|
+
"""
|
|
26
|
+
Loader which uploads files into OpenAI Files API and returns openai_file_id.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client: AsyncOpenAI, *, purpose: AllowedPurpose = "assistants") -> None:
|
|
30
|
+
self.client = client
|
|
31
|
+
self.purpose: AllowedPurpose = purpose # mypy: Literal union, matches SDK
|
|
32
|
+
|
|
33
|
+
async def _upload_one(self, file: BinaryIO, *, filename: str | None, filedata: FileData | None) -> FileAttachment:
|
|
34
|
+
try:
|
|
35
|
+
if hasattr(file, "seek"):
|
|
36
|
+
file.seek(0)
|
|
37
|
+
except Exception as exc: # pragma: no cover
|
|
38
|
+
logger.debug("seek(0) failed for %r: %s", filename or file, exc)
|
|
39
|
+
|
|
40
|
+
buf = file if isinstance(file, io.BytesIO) else io.BytesIO(file.read())
|
|
41
|
+
|
|
42
|
+
up = await self.client.files.create(file=(filename or "upload.bin", buf), purpose=self.purpose)
|
|
43
|
+
|
|
44
|
+
meta: dict[str, Any] = {
|
|
45
|
+
"filename": filename,
|
|
46
|
+
"provider": "openai",
|
|
47
|
+
"file": {
|
|
48
|
+
"id": up.id,
|
|
49
|
+
"bytes": getattr(up, "bytes", None),
|
|
50
|
+
"purpose": getattr(up, "purpose", self.purpose),
|
|
51
|
+
"created_at": getattr(up, "created_at", None),
|
|
52
|
+
"status": getattr(up, "status", None),
|
|
53
|
+
"status_details": getattr(up, "status_details", None),
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
if filedata is not None:
|
|
57
|
+
meta["filedata"] = filedata.model_dump()
|
|
58
|
+
|
|
59
|
+
return FileAttachment(type=FILE_ID, content=up.id, metadata=meta)
|
|
60
|
+
|
|
61
|
+
async def load(self, item: FileItem) -> FileAttachment:
|
|
62
|
+
return await self._upload_one(item.file, filename=item.filename, filedata=item.filedata)
|
|
63
|
+
|
|
64
|
+
async def load_batch(self, items: Sequence[FileItem]) -> list[FileAttachment]:
|
|
65
|
+
tasks = [
|
|
66
|
+
asyncio.create_task(self._upload_one(it.file, filename=it.filename, filedata=it.filedata))
|
|
67
|
+
for it in items
|
|
68
|
+
]
|
|
69
|
+
return await asyncio.gather(*tasks)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import base64
|
|
4
5
|
from collections.abc import Iterable
|
|
5
6
|
from contextlib import AsyncExitStack
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
9
|
+
from amsdal_utils.config.manager import AmsdalConfigManager
|
|
8
10
|
from mcp import ClientSession
|
|
9
11
|
from mcp import StdioServerParameters
|
|
10
12
|
from mcp.client.stdio import stdio_client
|
|
@@ -19,7 +21,14 @@ class StdioClient(ToolClient):
|
|
|
19
21
|
MCP over STDIO client.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
|
-
def __init__(
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
alias: str,
|
|
27
|
+
module_or_cmd: str,
|
|
28
|
+
*args: str,
|
|
29
|
+
persist_session: bool = True,
|
|
30
|
+
send_amsdal_config: bool = True,
|
|
31
|
+
):
|
|
23
32
|
self.alias = alias
|
|
24
33
|
if module_or_cmd in ("python", "python3"):
|
|
25
34
|
self._command = module_or_cmd
|
|
@@ -27,7 +36,9 @@ class StdioClient(ToolClient):
|
|
|
27
36
|
else:
|
|
28
37
|
self._command = "python"
|
|
29
38
|
self._args = ["-m", module_or_cmd]
|
|
30
|
-
|
|
39
|
+
if send_amsdal_config:
|
|
40
|
+
self._args.append("--amsdal-config")
|
|
41
|
+
self._args.append(self._build_amsdal_config_arg())
|
|
31
42
|
self._persist = persist_session
|
|
32
43
|
self._lock = asyncio.Lock()
|
|
33
44
|
self._stack: AsyncExitStack | None = None
|
|
@@ -115,6 +126,7 @@ class StdioClient(ToolClient):
|
|
|
115
126
|
rx, tx = await stack.enter_async_context(stdio_client(params))
|
|
116
127
|
s = await stack.enter_async_context(ClientSession(rx, tx))
|
|
117
128
|
await s.initialize()
|
|
129
|
+
print("Calling tool:", tool_name, "with args:", args) # noqa: T201
|
|
118
130
|
res = await self._call_with_timeout(s.call_tool(tool_name, args), timeout=timeout)
|
|
119
131
|
return getattr(res, "content", res)
|
|
120
132
|
|
|
@@ -128,3 +140,11 @@ class StdioClient(ToolClient):
|
|
|
128
140
|
s = await self._ensure_session()
|
|
129
141
|
res = await self._call_with_timeout(s.call_tool(tool_name, args), timeout=timeout)
|
|
130
142
|
return getattr(res, "content", res)
|
|
143
|
+
|
|
144
|
+
def _build_amsdal_config_arg(self) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Build a JSON string argument representing the current Amsdal configuration.
|
|
147
|
+
This can be passed to the subprocess to ensure it has the same configuration context.
|
|
148
|
+
"""
|
|
149
|
+
config = AmsdalConfigManager().get_config()
|
|
150
|
+
return base64.b64encode(config.model_dump_json().encode('utf-8')).decode('utf-8')
|
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from amsdal.manager import AmsdalManager
|
|
11
|
+
from amsdal.manager import AsyncAmsdalManager
|
|
12
|
+
from amsdal_utils.config.data_models.amsdal_config import AmsdalConfig
|
|
13
|
+
from amsdal_utils.config.manager import AmsdalConfigManager
|
|
1
14
|
from mcp.server.fastmcp import FastMCP
|
|
2
15
|
|
|
3
16
|
from amsdal_ml.agents.retriever_tool import retriever_search
|
|
4
17
|
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=logging.INFO,
|
|
20
|
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
21
|
+
handlers=[
|
|
22
|
+
logging.FileHandler("server.log"),
|
|
23
|
+
logging.StreamHandler(sys.stdout)
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser()
|
|
28
|
+
parser.add_argument("--amsdal-config", required=False, help="Base64-encoded config string")
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
logging.info(f"Starting server with args: {args}")
|
|
32
|
+
|
|
33
|
+
if args.amsdal_config:
|
|
34
|
+
decoded = base64.b64decode(args.amsdal_config).decode("utf-8")
|
|
35
|
+
amsdal_config = AmsdalConfig(**json.loads(decoded))
|
|
36
|
+
logging.info(f"Loaded Amsdal config: {amsdal_config}")
|
|
37
|
+
AmsdalConfigManager().set_config(amsdal_config)
|
|
38
|
+
|
|
39
|
+
manager: Any
|
|
40
|
+
if amsdal_config.async_mode:
|
|
41
|
+
manager = AsyncAmsdalManager()
|
|
42
|
+
logging.info("pre-setup")
|
|
43
|
+
asyncio.run(cast(Any, manager).setup())
|
|
44
|
+
logging.info("post-setup")
|
|
45
|
+
asyncio.run(cast(Any, manager).post_setup())
|
|
46
|
+
logging.info("manager inited")
|
|
47
|
+
else:
|
|
48
|
+
manager = AmsdalManager()
|
|
49
|
+
cast(Any, manager).setup()
|
|
50
|
+
cast(Any, manager).post_setup()
|
|
51
|
+
|
|
5
52
|
server = FastMCP('retriever-stdio')
|
|
6
53
|
server.tool(
|
|
7
|
-
name='
|
|
54
|
+
name='search',
|
|
8
55
|
description='Semantic search in knowledge base (OpenAI embeddings)',
|
|
9
56
|
structured_output=True,
|
|
10
57
|
)(retriever_search)
|
|
58
|
+
|
|
11
59
|
server.run(transport='stdio')
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from amsdal_models.migration import migrations
|
|
2
|
+
from amsdal_utils.models.enums import ModuleType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
operations: list[migrations.Operation] = [
|
|
7
|
+
migrations.CreateClass(
|
|
8
|
+
module_type=ModuleType.CONTRIB,
|
|
9
|
+
class_name="EmbeddingModel",
|
|
10
|
+
new_schema={
|
|
11
|
+
"title": "EmbeddingModel",
|
|
12
|
+
"required": ["data_object_class", "data_object_id", "chunk_index", "raw_text", "embedding"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"data_object_class": {"type": "string", "title": "Linked object class"},
|
|
15
|
+
"data_object_id": {"type": "string", "title": "Linked object ID"},
|
|
16
|
+
"chunk_index": {"type": "integer", "title": "Chunk index"},
|
|
17
|
+
"raw_text": {"type": "string", "title": "Raw text used for embedding"},
|
|
18
|
+
"embedding": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {"type": "number"},
|
|
21
|
+
"title": "Embedding",
|
|
22
|
+
"additional_type": "vector",
|
|
23
|
+
"dimensions": 1536,
|
|
24
|
+
},
|
|
25
|
+
"tags": {"type": "array", "items": {"type": "string"}, "title": "Embedding tags"},
|
|
26
|
+
"ml_metadata": {"type": "anything", "title": "ML metadata"},
|
|
27
|
+
},
|
|
28
|
+
"storage_metadata": {
|
|
29
|
+
"table_name": "embedding_model",
|
|
30
|
+
"db_fields": {},
|
|
31
|
+
"primary_key": ["partition_key"],
|
|
32
|
+
"foreign_keys": {},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
]
|
|
@@ -288,8 +288,8 @@ class DefaultIngesting(MLIngesting):
|
|
|
288
288
|
object_class, object_id = self._resolve_link(instance)
|
|
289
289
|
objs = [
|
|
290
290
|
EmbeddingModel(
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
data_object_class=object_class,
|
|
292
|
+
data_object_id=object_id,
|
|
293
293
|
chunk_index=r.chunk_index,
|
|
294
294
|
raw_text=r.raw_text,
|
|
295
295
|
embedding=r.embedding,
|
|
@@ -305,8 +305,8 @@ class DefaultIngesting(MLIngesting):
|
|
|
305
305
|
object_class, object_id = self._resolve_link(instance)
|
|
306
306
|
objs = [
|
|
307
307
|
EmbeddingModel(
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
data_object_class=object_class,
|
|
309
|
+
data_object_id=object_id,
|
|
310
310
|
chunk_index=r.chunk_index,
|
|
311
311
|
raw_text=r.raw_text,
|
|
312
312
|
embedding=r.embedding,
|
amsdal_ml/ml_models/models.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from abc import ABC
|
|
2
4
|
from abc import abstractmethod
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
7
|
+
from amsdal_ml.fileio.base_loader import PLAIN_TEXT
|
|
8
|
+
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
3
9
|
|
|
4
10
|
|
|
5
11
|
class ModelError(Exception):
|
|
@@ -20,31 +26,62 @@ class ModelAPIError(ModelError):
|
|
|
20
26
|
|
|
21
27
|
class MLModel(ABC):
|
|
22
28
|
@abstractmethod
|
|
23
|
-
def setup(self):
|
|
29
|
+
def setup(self) -> None:
|
|
24
30
|
"""Initialize any clients or resources needed before inference."""
|
|
25
|
-
|
|
31
|
+
raise NotImplementedError
|
|
26
32
|
|
|
27
33
|
@abstractmethod
|
|
28
|
-
def teardown(self):
|
|
34
|
+
def teardown(self) -> None:
|
|
29
35
|
"""Clean up resources after use."""
|
|
30
|
-
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
def supported_attachments(self) -> set[str]:
|
|
39
|
+
"""Return a set of universal attachment kinds, e.g. {PLAIN_TEXT, FILE_ID}."""
|
|
40
|
+
return {PLAIN_TEXT}
|
|
31
41
|
|
|
32
42
|
@abstractmethod
|
|
33
|
-
def invoke(
|
|
43
|
+
def invoke(
|
|
44
|
+
self,
|
|
45
|
+
prompt: str,
|
|
46
|
+
*,
|
|
47
|
+
attachments: list[FileAttachment] | None = None,
|
|
48
|
+
) -> str:
|
|
34
49
|
"""Run synchronous inference with the model."""
|
|
35
|
-
|
|
50
|
+
raise NotImplementedError
|
|
36
51
|
|
|
37
52
|
@abstractmethod
|
|
38
|
-
async def ainvoke(
|
|
53
|
+
async def ainvoke(
|
|
54
|
+
self,
|
|
55
|
+
prompt: str,
|
|
56
|
+
*,
|
|
57
|
+
attachments: list[FileAttachment] | None = None,
|
|
58
|
+
) -> str:
|
|
39
59
|
"""Run asynchronous inference with the model."""
|
|
40
|
-
|
|
60
|
+
raise NotImplementedError
|
|
41
61
|
|
|
42
62
|
@abstractmethod
|
|
43
|
-
def stream(
|
|
63
|
+
def stream(
|
|
64
|
+
self,
|
|
65
|
+
prompt: str,
|
|
66
|
+
*,
|
|
67
|
+
attachments: list[FileAttachment] | None = None,
|
|
68
|
+
):
|
|
44
69
|
"""Stream synchronous inference results from the model."""
|
|
45
|
-
|
|
70
|
+
raise NotImplementedError
|
|
46
71
|
|
|
47
72
|
@abstractmethod
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
def astream(
|
|
74
|
+
self,
|
|
75
|
+
prompt: str,
|
|
76
|
+
*,
|
|
77
|
+
attachments: list[FileAttachment] | None = None,
|
|
78
|
+
) -> AsyncIterator[str]:
|
|
79
|
+
"""
|
|
80
|
+
Stream asynchronous inference results as an async generator.
|
|
81
|
+
|
|
82
|
+
Subclasses should implement this like:
|
|
83
|
+
|
|
84
|
+
async def astream(... ) -> AsyncIterator[str]:
|
|
85
|
+
yield "chunk"
|
|
86
|
+
"""
|
|
87
|
+
raise NotImplementedError
|
|
@@ -2,12 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from typing import Any
|
|
5
8
|
from typing import Optional
|
|
9
|
+
from typing import cast
|
|
6
10
|
|
|
7
11
|
import openai
|
|
8
12
|
from openai import AsyncOpenAI
|
|
13
|
+
from openai import AsyncStream
|
|
9
14
|
from openai import OpenAI
|
|
15
|
+
from openai import Stream
|
|
10
16
|
|
|
17
|
+
from amsdal_ml.fileio.base_loader import FILE_ID
|
|
18
|
+
from amsdal_ml.fileio.base_loader import PLAIN_TEXT
|
|
19
|
+
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
11
20
|
from amsdal_ml.ml_config import ml_config
|
|
12
21
|
from amsdal_ml.ml_models.models import MLModel
|
|
13
22
|
from amsdal_ml.ml_models.models import ModelAPIError
|
|
@@ -17,13 +26,110 @@ from amsdal_ml.ml_models.models import ModelRateLimitError
|
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
class OpenAIModel(MLModel):
|
|
20
|
-
|
|
29
|
+
"""OpenAI LLM wrapper using a single Responses API pathway for all modes."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
21
32
|
self.client: Optional[OpenAI | AsyncOpenAI] = None
|
|
22
33
|
self.async_mode: bool = bool(ml_config.async_mode)
|
|
23
34
|
self.model_name: str = ml_config.llm_model_name
|
|
24
35
|
self.temperature: float = ml_config.llm_temperature
|
|
25
36
|
self._api_key: Optional[str] = None
|
|
26
37
|
|
|
38
|
+
# ---------- Public sync API ----------
|
|
39
|
+
def invoke(
|
|
40
|
+
self,
|
|
41
|
+
prompt: str,
|
|
42
|
+
*,
|
|
43
|
+
attachments: list[FileAttachment] | None = None,
|
|
44
|
+
) -> str:
|
|
45
|
+
if self.async_mode:
|
|
46
|
+
msg = "Async mode is enabled. Use 'ainvoke' instead."
|
|
47
|
+
raise RuntimeError(msg)
|
|
48
|
+
if not isinstance(self.client, OpenAI):
|
|
49
|
+
msg = "Sync client is not initialized. Call setup() first."
|
|
50
|
+
raise RuntimeError(msg)
|
|
51
|
+
|
|
52
|
+
atts = self._validate_attachments(attachments)
|
|
53
|
+
if self._has_file_ids(atts):
|
|
54
|
+
input_content = self._build_input_content(prompt, atts)
|
|
55
|
+
return self._call_responses(input_content)
|
|
56
|
+
|
|
57
|
+
final_prompt = self._merge_plain_text(prompt, atts)
|
|
58
|
+
return self._call_chat(final_prompt)
|
|
59
|
+
|
|
60
|
+
def stream(
|
|
61
|
+
self,
|
|
62
|
+
prompt: str,
|
|
63
|
+
*,
|
|
64
|
+
attachments: list[FileAttachment] | None = None,
|
|
65
|
+
) -> Iterator[str]:
|
|
66
|
+
if self.async_mode:
|
|
67
|
+
msg = "Async mode is enabled. Use 'astream' instead."
|
|
68
|
+
raise RuntimeError(msg)
|
|
69
|
+
if not isinstance(self.client, OpenAI):
|
|
70
|
+
msg = "Sync client is not initialized. Call setup() first."
|
|
71
|
+
raise RuntimeError(msg)
|
|
72
|
+
|
|
73
|
+
atts = self._validate_attachments(attachments)
|
|
74
|
+
if self._has_file_ids(atts):
|
|
75
|
+
input_content = self._build_input_content(prompt, atts)
|
|
76
|
+
for chunk in self._call_responses_stream(input_content):
|
|
77
|
+
yield chunk
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
final_prompt = self._merge_plain_text(prompt, atts)
|
|
81
|
+
for chunk in self._call_chat_stream(final_prompt):
|
|
82
|
+
yield chunk
|
|
83
|
+
|
|
84
|
+
# ---------- Public async API ----------
|
|
85
|
+
async def ainvoke(
|
|
86
|
+
self,
|
|
87
|
+
prompt: str,
|
|
88
|
+
*,
|
|
89
|
+
attachments: list[FileAttachment] | None = None,
|
|
90
|
+
) -> str:
|
|
91
|
+
if not self.async_mode:
|
|
92
|
+
msg = "Async mode is disabled. Use 'invoke' instead."
|
|
93
|
+
raise RuntimeError(msg)
|
|
94
|
+
self._ensure_async_client()
|
|
95
|
+
if not isinstance(self.client, AsyncOpenAI):
|
|
96
|
+
msg = "Async client is not initialized. Call setup() first."
|
|
97
|
+
raise RuntimeError(msg)
|
|
98
|
+
|
|
99
|
+
atts = self._validate_attachments(attachments)
|
|
100
|
+
if self._has_file_ids(atts):
|
|
101
|
+
input_content = self._build_input_content(prompt, atts)
|
|
102
|
+
return await self._acall_responses(input_content)
|
|
103
|
+
|
|
104
|
+
final_prompt = self._merge_plain_text(prompt, atts)
|
|
105
|
+
return await self._acall_chat(final_prompt)
|
|
106
|
+
|
|
107
|
+
async def astream(
|
|
108
|
+
self,
|
|
109
|
+
prompt: str,
|
|
110
|
+
*,
|
|
111
|
+
attachments: list[FileAttachment] | None = None,
|
|
112
|
+
) -> AsyncIterator[str]:
|
|
113
|
+
if not self.async_mode:
|
|
114
|
+
msg = "Async mode is disabled. Use 'stream' instead."
|
|
115
|
+
raise RuntimeError(msg)
|
|
116
|
+
self._ensure_async_client()
|
|
117
|
+
if not isinstance(self.client, AsyncOpenAI):
|
|
118
|
+
msg = "Async client is not initialized. Call setup() first."
|
|
119
|
+
raise RuntimeError(msg)
|
|
120
|
+
|
|
121
|
+
atts = self._validate_attachments(attachments)
|
|
122
|
+
if self._has_file_ids(atts):
|
|
123
|
+
input_content = self._build_input_content(prompt, atts)
|
|
124
|
+
async for chunk in self._acall_responses_stream(input_content):
|
|
125
|
+
yield chunk
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
final_prompt = self._merge_plain_text(prompt, atts)
|
|
129
|
+
async for chunk in self._acall_chat_stream(final_prompt):
|
|
130
|
+
yield chunk
|
|
131
|
+
|
|
132
|
+
# ---------- lifecycle ----------
|
|
27
133
|
def setup(self) -> None:
|
|
28
134
|
api_key = os.getenv("OPENAI_API_KEY") or ml_config.resolved_openai_key
|
|
29
135
|
if not api_key:
|
|
@@ -36,6 +142,7 @@ class OpenAIModel(MLModel):
|
|
|
36
142
|
|
|
37
143
|
try:
|
|
38
144
|
if self.async_mode:
|
|
145
|
+
# Only create async client if loop is running; otherwise defer.
|
|
39
146
|
try:
|
|
40
147
|
asyncio.get_running_loop()
|
|
41
148
|
self._ensure_async_client()
|
|
@@ -43,27 +150,88 @@ class OpenAIModel(MLModel):
|
|
|
43
150
|
self.client = None
|
|
44
151
|
else:
|
|
45
152
|
self.client = OpenAI(api_key=self._api_key)
|
|
46
|
-
except Exception as e:
|
|
153
|
+
except Exception as e: # pragma: no cover
|
|
47
154
|
raise self._map_openai_error(e) from e
|
|
48
155
|
|
|
49
156
|
def _ensure_async_client(self) -> None:
|
|
50
157
|
if self.client is None:
|
|
51
158
|
try:
|
|
52
159
|
self.client = AsyncOpenAI(api_key=self._api_key)
|
|
53
|
-
except Exception as e:
|
|
160
|
+
except Exception as e: # pragma: no cover
|
|
54
161
|
raise self._map_openai_error(e) from e
|
|
55
162
|
|
|
56
163
|
def teardown(self) -> None:
|
|
57
164
|
self.client = None
|
|
58
165
|
|
|
166
|
+
def _require_sync_client(self) -> OpenAI:
|
|
167
|
+
if not isinstance(self.client, OpenAI):
|
|
168
|
+
msg = "Sync client is not initialized. Call setup() first."
|
|
169
|
+
raise RuntimeError(msg)
|
|
170
|
+
return self.client
|
|
171
|
+
|
|
172
|
+
def _require_async_client(self) -> AsyncOpenAI:
|
|
173
|
+
if not isinstance(self.client, AsyncOpenAI):
|
|
174
|
+
msg = "Async client is not initialized. Call setup() first."
|
|
175
|
+
raise RuntimeError(msg)
|
|
176
|
+
return self.client
|
|
177
|
+
|
|
178
|
+
# ---------- attachments ----------
|
|
179
|
+
def supported_attachments(self) -> set[str]:
|
|
180
|
+
# Universal kinds supported by this model
|
|
181
|
+
return {PLAIN_TEXT, FILE_ID}
|
|
182
|
+
|
|
183
|
+
def _validate_attachments(
|
|
184
|
+
self, attachments: list[FileAttachment] | None
|
|
185
|
+
) -> list[FileAttachment]:
|
|
186
|
+
atts = attachments or []
|
|
187
|
+
kinds = {a.type for a in atts}
|
|
188
|
+
unsupported = kinds - self.supported_attachments()
|
|
189
|
+
if unsupported:
|
|
190
|
+
msg = (
|
|
191
|
+
f"{self.__class__.__name__} does not support attachments: "
|
|
192
|
+
f"{', '.join(sorted(unsupported))}"
|
|
193
|
+
)
|
|
194
|
+
raise ModelAPIError(msg)
|
|
195
|
+
|
|
196
|
+
foreign = [
|
|
197
|
+
a for a in atts if a.type == FILE_ID and (a.metadata or {}).get("provider") != "openai"
|
|
198
|
+
]
|
|
199
|
+
if foreign:
|
|
200
|
+
provs = {(a.metadata or {}).get("provider", "unknown") for a in foreign}
|
|
201
|
+
msg = (
|
|
202
|
+
f"{self.__class__.__name__} only supports FILE_ID with provider='openai'. "
|
|
203
|
+
f"Got providers: {', '.join(sorted(provs))}"
|
|
204
|
+
)
|
|
205
|
+
raise ModelAPIError(msg)
|
|
206
|
+
|
|
207
|
+
return atts
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _has_file_ids(atts: list[FileAttachment]) -> bool:
|
|
211
|
+
return any(a.type == FILE_ID for a in atts)
|
|
212
|
+
|
|
213
|
+
def _build_input_content(self, prompt: str, atts: list[FileAttachment]) -> list[dict[str, Any]]:
|
|
214
|
+
parts: list[dict[str, Any]] = [{"type": "input_text", "text": prompt}]
|
|
215
|
+
for a in atts:
|
|
216
|
+
if a.type == PLAIN_TEXT:
|
|
217
|
+
parts.append({"type": "input_text", "text": str(a.content)})
|
|
218
|
+
elif a.type == FILE_ID:
|
|
219
|
+
parts.append({"type": "input_file", "file_id": str(a.content)})
|
|
220
|
+
return [{"role": "user", "content": parts}]
|
|
221
|
+
|
|
222
|
+
def _merge_plain_text(self, prompt: str, atts: list[FileAttachment]) -> str:
|
|
223
|
+
extras = [str(a.content) for a in atts if a.type == PLAIN_TEXT]
|
|
224
|
+
if not extras:
|
|
225
|
+
return prompt
|
|
226
|
+
return f"{prompt}\n\n[ATTACHMENTS]\n" + "\n\n".join(extras)
|
|
227
|
+
|
|
228
|
+
# ---------- error mapping ----------
|
|
59
229
|
@staticmethod
|
|
60
230
|
def _map_openai_error(err: Exception) -> ModelError:
|
|
61
231
|
if isinstance(err, openai.RateLimitError):
|
|
62
232
|
return ModelRateLimitError(str(err))
|
|
63
|
-
|
|
64
233
|
if isinstance(err, openai.APIConnectionError):
|
|
65
234
|
return ModelConnectionError(str(err))
|
|
66
|
-
|
|
67
235
|
if isinstance(err, openai.APIStatusError):
|
|
68
236
|
status = getattr(err, "status_code", None)
|
|
69
237
|
resp = getattr(err, "response", None)
|
|
@@ -72,26 +240,16 @@ class OpenAIModel(MLModel):
|
|
|
72
240
|
payload_repr = resp.json() if resp is not None else None
|
|
73
241
|
except Exception:
|
|
74
242
|
payload_repr = None
|
|
75
|
-
|
|
76
|
-
return ModelAPIError(msg)
|
|
77
|
-
|
|
243
|
+
return ModelAPIError(f"OpenAI API status error ({status}). payload={payload_repr!r}")
|
|
78
244
|
if isinstance(err, openai.APIError):
|
|
79
245
|
return ModelAPIError(str(err))
|
|
80
|
-
|
|
81
246
|
return ModelAPIError(str(err))
|
|
82
247
|
|
|
83
|
-
# ---------- Sync ----------
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
msg = "Async mode is enabled. Use 'ainvoke' instead."
|
|
87
|
-
raise RuntimeError(msg)
|
|
88
|
-
|
|
89
|
-
if not isinstance(self.client, OpenAI):
|
|
90
|
-
msg = "Sync client is not initialized. Call setup() first."
|
|
91
|
-
raise RuntimeError(msg)
|
|
92
|
-
|
|
248
|
+
# ---------- Sync core callers ----------
|
|
249
|
+
def _call_chat(self, prompt: str) -> str:
|
|
250
|
+
client = self._require_sync_client()
|
|
93
251
|
try:
|
|
94
|
-
resp =
|
|
252
|
+
resp = client.chat.completions.create(
|
|
95
253
|
model=self.model_name,
|
|
96
254
|
messages=[{"role": "user", "content": prompt}],
|
|
97
255
|
temperature=self.temperature,
|
|
@@ -100,17 +258,10 @@ class OpenAIModel(MLModel):
|
|
|
100
258
|
except Exception as e:
|
|
101
259
|
raise self._map_openai_error(e) from e
|
|
102
260
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
msg = "Async mode is enabled. Use 'astream' instead."
|
|
106
|
-
raise RuntimeError(msg)
|
|
107
|
-
|
|
108
|
-
if not isinstance(self.client, OpenAI):
|
|
109
|
-
msg = "Sync client is not initialized. Call setup() first."
|
|
110
|
-
raise RuntimeError(msg)
|
|
111
|
-
|
|
261
|
+
def _call_chat_stream(self, prompt: str) -> Iterator[str]:
|
|
262
|
+
client = self._require_sync_client()
|
|
112
263
|
try:
|
|
113
|
-
stream =
|
|
264
|
+
stream = client.chat.completions.create(
|
|
114
265
|
model=self.model_name,
|
|
115
266
|
messages=[{"role": "user", "content": prompt}],
|
|
116
267
|
temperature=self.temperature,
|
|
@@ -123,18 +274,43 @@ class OpenAIModel(MLModel):
|
|
|
123
274
|
except Exception as e:
|
|
124
275
|
raise self._map_openai_error(e) from e
|
|
125
276
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
277
|
+
def _call_responses(self, input_content: list[dict[str, Any]]) -> str:
|
|
278
|
+
client = self._require_sync_client()
|
|
279
|
+
try:
|
|
280
|
+
resp: Any = client.responses.create(
|
|
281
|
+
model=self.model_name,
|
|
282
|
+
input=cast(Any, input_content),
|
|
283
|
+
temperature=self.temperature,
|
|
284
|
+
)
|
|
285
|
+
return (getattr(resp, "output_text", None) or "").strip()
|
|
286
|
+
except Exception as e:
|
|
287
|
+
raise self._map_openai_error(e) from e
|
|
131
288
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
289
|
+
def _call_responses_stream(self, input_content: list[dict[str, Any]]) -> Iterator[str]:
|
|
290
|
+
client = self._require_sync_client()
|
|
291
|
+
try:
|
|
292
|
+
stream_or_resp = client.responses.create(
|
|
293
|
+
model=self.model_name,
|
|
294
|
+
input=cast(Any, input_content),
|
|
295
|
+
temperature=self.temperature,
|
|
296
|
+
stream=True,
|
|
297
|
+
)
|
|
298
|
+
if isinstance(stream_or_resp, Stream):
|
|
299
|
+
for ev in stream_or_resp:
|
|
300
|
+
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
301
|
+
if delta:
|
|
302
|
+
yield delta
|
|
303
|
+
else:
|
|
304
|
+
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
305
|
+
if text:
|
|
306
|
+
yield text
|
|
307
|
+
except Exception as e:
|
|
308
|
+
raise self._map_openai_error(e) from e
|
|
136
309
|
|
|
137
|
-
|
|
310
|
+
# ---------- Async core callers ----------
|
|
311
|
+
async def _acall_chat(self, prompt: str) -> str:
|
|
312
|
+
client = self._require_async_client()
|
|
313
|
+
print("acall_chat:", prompt) # noqa: T201
|
|
138
314
|
try:
|
|
139
315
|
resp = await client.chat.completions.create(
|
|
140
316
|
model=self.model_name,
|
|
@@ -145,17 +321,8 @@ class OpenAIModel(MLModel):
|
|
|
145
321
|
except Exception as e:
|
|
146
322
|
raise self._map_openai_error(e) from e
|
|
147
323
|
|
|
148
|
-
async def
|
|
149
|
-
|
|
150
|
-
msg = "Async mode is disabled. Use 'stream' instead."
|
|
151
|
-
raise RuntimeError(msg)
|
|
152
|
-
|
|
153
|
-
self._ensure_async_client()
|
|
154
|
-
if not isinstance(self.client, AsyncOpenAI):
|
|
155
|
-
msg = "Async client is not initialized. Call setup() first."
|
|
156
|
-
raise RuntimeError(msg)
|
|
157
|
-
|
|
158
|
-
client = self.client
|
|
324
|
+
async def _acall_chat_stream(self, prompt: str) -> AsyncIterator[str]:
|
|
325
|
+
client = self._require_async_client()
|
|
159
326
|
try:
|
|
160
327
|
stream = await client.chat.completions.create(
|
|
161
328
|
model=self.model_name,
|
|
@@ -169,3 +336,36 @@ class OpenAIModel(MLModel):
|
|
|
169
336
|
yield delta.content
|
|
170
337
|
except Exception as e:
|
|
171
338
|
raise self._map_openai_error(e) from e
|
|
339
|
+
|
|
340
|
+
async def _acall_responses(self, input_content: list[dict[str, Any]]) -> str:
|
|
341
|
+
client = self._require_async_client()
|
|
342
|
+
try:
|
|
343
|
+
resp: Any = await client.responses.create(
|
|
344
|
+
model=self.model_name,
|
|
345
|
+
input=cast(Any, input_content),
|
|
346
|
+
temperature=self.temperature,
|
|
347
|
+
)
|
|
348
|
+
return (getattr(resp, "output_text", None) or "").strip()
|
|
349
|
+
except Exception as e:
|
|
350
|
+
raise self._map_openai_error(e) from e
|
|
351
|
+
|
|
352
|
+
async def _acall_responses_stream(self, input_content: list[dict[str, Any]]) -> AsyncIterator[str]:
|
|
353
|
+
client = self._require_async_client()
|
|
354
|
+
try:
|
|
355
|
+
stream_or_resp = await client.responses.create(
|
|
356
|
+
model=self.model_name,
|
|
357
|
+
input=cast(Any, input_content),
|
|
358
|
+
temperature=self.temperature,
|
|
359
|
+
stream=True,
|
|
360
|
+
)
|
|
361
|
+
if isinstance(stream_or_resp, AsyncStream):
|
|
362
|
+
async for ev in stream_or_resp:
|
|
363
|
+
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
364
|
+
if delta:
|
|
365
|
+
yield delta
|
|
366
|
+
else:
|
|
367
|
+
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
368
|
+
if text:
|
|
369
|
+
yield text
|
|
370
|
+
except Exception as e:
|
|
371
|
+
raise self._map_openai_error(e) from e
|
|
@@ -53,15 +53,15 @@ class DefaultRetriever(MLRetriever, ABC):
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
pre = max(k * 5, 100)
|
|
56
|
-
rows = list(qs.execute())
|
|
56
|
+
rows = list(qs[:pre].execute())
|
|
57
57
|
|
|
58
58
|
rows = self._filter_rows_by_tags(rows, include_tags, exclude_tags)
|
|
59
59
|
rows = rows[:k]
|
|
60
60
|
|
|
61
61
|
return [
|
|
62
62
|
RetrievalChunk(
|
|
63
|
-
object_class=r.
|
|
64
|
-
object_id=r.
|
|
63
|
+
object_class=r.data_object_class,
|
|
64
|
+
object_id=r.data_object_id,
|
|
65
65
|
chunk_index=r.chunk_index,
|
|
66
66
|
raw_text=(r.raw_text or '').strip(),
|
|
67
67
|
distance=float(getattr(r, 'distance', math.inf)),
|
|
@@ -94,8 +94,8 @@ class DefaultRetriever(MLRetriever, ABC):
|
|
|
94
94
|
|
|
95
95
|
return [
|
|
96
96
|
RetrievalChunk(
|
|
97
|
-
object_class=r.
|
|
98
|
-
object_id=r.
|
|
97
|
+
object_class=r.data_object_class,
|
|
98
|
+
object_id=r.data_object_id,
|
|
99
99
|
chunk_index=r.chunk_index,
|
|
100
100
|
raw_text=(r.raw_text or '').strip(),
|
|
101
101
|
distance=float(getattr(r, 'distance', math.inf)),
|
|
@@ -10,8 +10,8 @@ class EmbeddingModel(Model):
|
|
|
10
10
|
__module_type__ = ModuleType.CONTRIB
|
|
11
11
|
__table_name__ = "embedding_model"
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
data_object_class: str = Field(..., title="Linked object class")
|
|
14
|
+
data_object_id: str = Field(..., title="Linked object ID")
|
|
15
15
|
|
|
16
16
|
chunk_index: int = Field(..., title="Chunk index")
|
|
17
17
|
raw_text: str = Field(..., title="Raw text used for embedding")
|
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
amsdal_ml/Third-Party Materials - AMSDAL Dependencies - License Notices.md,sha256=0A3jjw91I-NaTHXCw8IQiKUdFJgVavhgHaFX4Nag4jk,81658
|
|
2
|
-
amsdal_ml/__about__.py,sha256=
|
|
2
|
+
amsdal_ml/__about__.py,sha256=mdp2CftfqYbdKtP-eWv1z7rAUycYv6X1ntXSMUf8Kss,22
|
|
3
3
|
amsdal_ml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
amsdal_ml/app.py,sha256=yq_XIstjXSKplsjHLn9Jb65zXx4HOdjPKVlbz5FT9ec,715
|
|
5
5
|
amsdal_ml/ml_config.py,sha256=UONMVG2RbKVbaYF-GdlhRQbm8flp52GxBzfqdzuLZ5w,1788
|
|
6
6
|
amsdal_ml/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
amsdal_ml/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
amsdal_ml/agents/agent.py,sha256=
|
|
9
|
-
amsdal_ml/agents/default_qa_agent.py,sha256=
|
|
10
|
-
amsdal_ml/agents/retriever_tool.py,sha256=
|
|
8
|
+
amsdal_ml/agents/agent.py,sha256=L00RTIkopdGnJp_NctEtWRXAgWy1DAn7AEg0rMoXdJw,1514
|
|
9
|
+
amsdal_ml/agents/default_qa_agent.py,sha256=MjrA5XYOKe5PwfSnkoeGmEnC8mP9asRHGHPHd1oPH6g,14017
|
|
10
|
+
amsdal_ml/agents/retriever_tool.py,sha256=SqIGqYi_xMcm_MgLcwdMW2ALzYphkk02KBevH8n-Op0,1850
|
|
11
11
|
amsdal_ml/agents/promts/__init__.py,sha256=3AKR3lrV067CO03XYFDmAKJv0x2oqSTcVeDIqNwoAu0,1571
|
|
12
|
-
amsdal_ml/agents/promts/react_chat.prompt,sha256=
|
|
12
|
+
amsdal_ml/agents/promts/react_chat.prompt,sha256=EArdA102sSBOWchhub1QoaKws_DVpMqDflc5htBkFKI,1762
|
|
13
|
+
amsdal_ml/fileio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
amsdal_ml/fileio/base_loader.py,sha256=rFeClp-ZkSX6Ja1vz1RutlyMqYIGgR48CrtXDWXr4Ns,1866
|
|
15
|
+
amsdal_ml/fileio/openai_loader.py,sha256=4YyxK5pBAnRm55et5yMu6X5OdK2kWTqpuPF1E2-oFZc,2572
|
|
13
16
|
amsdal_ml/mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
17
|
amsdal_ml/mcp_client/base.py,sha256=cNuHHCSSND4BkIyhQZ-1mJLPp630wTCtiCQSHnK2zmA,441
|
|
15
18
|
amsdal_ml/mcp_client/http_client.py,sha256=2fFCaubp_cFeyCqN9CP7bVLTp4UIIEhJPq9BVHBf6-w,1719
|
|
16
|
-
amsdal_ml/mcp_client/stdio_client.py,sha256=
|
|
19
|
+
amsdal_ml/mcp_client/stdio_client.py,sha256=bYfS2yCeJNl8fxBn6Yerg4RBZKbW2pgcdKiwX-NL6io,5744
|
|
17
20
|
amsdal_ml/mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
amsdal_ml/mcp_server/server_retriever_stdio.py,sha256=
|
|
21
|
+
amsdal_ml/mcp_server/server_retriever_stdio.py,sha256=iUQNwWEDZE0OIS9vyTyJp_EOn4kvl9Gn0Okzcq-ymo0,1762
|
|
22
|
+
amsdal_ml/migrations/0000_initial.py,sha256=UeEtqHrCgmTQyGnyV2jC7yt9Ce7Xm0-MyhOQU3rRrs8,1633
|
|
19
23
|
amsdal_ml/ml_ingesting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
amsdal_ml/ml_ingesting/default_ingesting.py,sha256=
|
|
24
|
+
amsdal_ml/ml_ingesting/default_ingesting.py,sha256=udVAyUbjeCrsP9pk-VbAH689M3VmBRhOFbl9Que9oK0,12859
|
|
21
25
|
amsdal_ml/ml_ingesting/embedding_data.py,sha256=ZWxhYq8mkFVC3HMM5nEiObgy62X8hLh1McaGegnRUsk,355
|
|
22
26
|
amsdal_ml/ml_ingesting/ingesting.py,sha256=h-GdNCtutO8C75jbLko40CMjHym9Xk_nlMdNYLfBs58,1458
|
|
23
27
|
amsdal_ml/ml_ingesting/openai_ingesting.py,sha256=_2dU2id2a3xR_y0GvZc5LUDw3_wI-wbhEFogEOKF-H0,1421
|
|
24
28
|
amsdal_ml/ml_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
amsdal_ml/ml_models/models.py,sha256=
|
|
26
|
-
amsdal_ml/ml_models/openai_model.py,sha256=
|
|
29
|
+
amsdal_ml/ml_models/models.py,sha256=gZzZrjsoAnFc5pPwqqLPNh5RYxwKKaFyNg56NJnsn4w,2286
|
|
30
|
+
amsdal_ml/ml_models/openai_model.py,sha256=yQ6r4K7AOeG3NfWT6pXgjkRPMT0FuQPqFUWeHSwz0u8,14295
|
|
27
31
|
amsdal_ml/ml_retrievers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
amsdal_ml/ml_retrievers/default_retriever.py,sha256=
|
|
32
|
+
amsdal_ml/ml_retrievers/default_retriever.py,sha256=ykAzi6gESyG1693nALpPJ9XhKV_We1yrNxbYo1eG748,3233
|
|
29
33
|
amsdal_ml/ml_retrievers/openai_retriever.py,sha256=oY71oPJdFuECWaJL_ByKZFE7HRJ-S6ypRzzJ7XjI7H8,1236
|
|
30
34
|
amsdal_ml/ml_retrievers/retriever.py,sha256=zHvaAOAD6H4IaIaYA_wy2e8tMmGtOnyNsDS2B4dQjBU,1005
|
|
31
35
|
amsdal_ml/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
amsdal_ml/models/embedding_model.py,sha256=
|
|
33
|
-
amsdal_ml-0.1.
|
|
34
|
-
amsdal_ml-0.1.
|
|
35
|
-
amsdal_ml-0.1.
|
|
36
|
+
amsdal_ml/models/embedding_model.py,sha256=mdMSYHrkSIKHi5mg4HqMaTTiPGqLYtRdRkbEK_trFWw,774
|
|
37
|
+
amsdal_ml-0.1.2.dist-info/METADATA,sha256=N0ZdcQ0_5Wr2qTYxPAQK08_x6QGKyQZZRNURJQCmxiY,1676
|
|
38
|
+
amsdal_ml-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
39
|
+
amsdal_ml-0.1.2.dist-info/RECORD,,
|
|
File without changes
|