llm4agents-sdk 2.0.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.
Files changed (44) hide show
  1. llm4agents_sdk-2.0.0/.github/workflows/publish.yml +23 -0
  2. llm4agents_sdk-2.0.0/.gitignore +8 -0
  3. llm4agents_sdk-2.0.0/.serena/.gitignore +2 -0
  4. llm4agents_sdk-2.0.0/.serena/memories/project/commands.md +44 -0
  5. llm4agents_sdk-2.0.0/.serena/memories/project/overview.md +41 -0
  6. llm4agents_sdk-2.0.0/.serena/memories/tasks/task-9-client-implementation.md +39 -0
  7. llm4agents_sdk-2.0.0/.serena/project.local.yml +5 -0
  8. llm4agents_sdk-2.0.0/.serena/project.yml +127 -0
  9. llm4agents_sdk-2.0.0/PKG-INFO +40 -0
  10. llm4agents_sdk-2.0.0/README.md +26 -0
  11. llm4agents_sdk-2.0.0/llm4agents/__init__.py +4 -0
  12. llm4agents_sdk-2.0.0/llm4agents/chat/__init__.py +0 -0
  13. llm4agents_sdk-2.0.0/llm4agents/chat/completions.py +25 -0
  14. llm4agents_sdk-2.0.0/llm4agents/chat/conversation.py +137 -0
  15. llm4agents_sdk-2.0.0/llm4agents/chat/types.py +44 -0
  16. llm4agents_sdk-2.0.0/llm4agents/client.py +63 -0
  17. llm4agents_sdk-2.0.0/llm4agents/errors.py +81 -0
  18. llm4agents_sdk-2.0.0/llm4agents/tools/__init__.py +4 -0
  19. llm4agents_sdk-2.0.0/llm4agents/tools/tools.py +99 -0
  20. llm4agents_sdk-2.0.0/llm4agents/tools/types.py +42 -0
  21. llm4agents_sdk-2.0.0/llm4agents/transfer/__init__.py +0 -0
  22. llm4agents_sdk-2.0.0/llm4agents/transfer/signer.py +87 -0
  23. llm4agents_sdk-2.0.0/llm4agents/transfer/transfer.py +47 -0
  24. llm4agents_sdk-2.0.0/llm4agents/transfer/types.py +69 -0
  25. llm4agents_sdk-2.0.0/llm4agents/transport/__init__.py +0 -0
  26. llm4agents_sdk-2.0.0/llm4agents/transport/http.py +85 -0
  27. llm4agents_sdk-2.0.0/llm4agents/transport/mcp.py +89 -0
  28. llm4agents_sdk-2.0.0/llm4agents/types.py +38 -0
  29. llm4agents_sdk-2.0.0/llm4agents/wallets/__init__.py +11 -0
  30. llm4agents_sdk-2.0.0/llm4agents/wallets/types.py +102 -0
  31. llm4agents_sdk-2.0.0/llm4agents/wallets/wallets.py +27 -0
  32. llm4agents_sdk-2.0.0/pyproject.toml +29 -0
  33. llm4agents_sdk-2.0.0/tests/__init__.py +0 -0
  34. llm4agents_sdk-2.0.0/tests/chat/__init__.py +0 -0
  35. llm4agents_sdk-2.0.0/tests/chat/test_completions.py +55 -0
  36. llm4agents_sdk-2.0.0/tests/chat/test_conversation.py +113 -0
  37. llm4agents_sdk-2.0.0/tests/test_client.py +38 -0
  38. llm4agents_sdk-2.0.0/tests/test_errors.py +34 -0
  39. llm4agents_sdk-2.0.0/tests/test_tools.py +145 -0
  40. llm4agents_sdk-2.0.0/tests/test_transfer.py +132 -0
  41. llm4agents_sdk-2.0.0/tests/test_wallets.py +83 -0
  42. llm4agents_sdk-2.0.0/tests/transport/__init__.py +0 -0
  43. llm4agents_sdk-2.0.0/tests/transport/test_http.py +39 -0
  44. llm4agents_sdk-2.0.0/tests/transport/test_mcp.py +56 -0
@@ -0,0 +1,23 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Install hatchling
17
+ run: pip install hatchling
18
+ - name: Build
19
+ run: python -m hatchling build
20
+ - name: Publish
21
+ uses: pypa/gh-action-pypi-publish@release/v1
22
+ with:
23
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ *.pyc
@@ -0,0 +1,2 @@
1
+ /cache
2
+ /project.local.yml
@@ -0,0 +1,44 @@
1
+ # Project Commands
2
+
3
+ ## Setup
4
+ ```bash
5
+ cd /home/soho/gitlab-repos/proxy-llm/llm4agents-sdk-python
6
+ source .venv/bin/activate
7
+ ```
8
+
9
+ ## Testing
10
+ ```bash
11
+ # Full test suite
12
+ pytest tests/ -v
13
+
14
+ # Single test file
15
+ pytest tests/test_client.py -v
16
+
17
+ # Single test by name
18
+ pytest tests/test_client.py::test_client_creates -v
19
+
20
+ # With asyncio info
21
+ pytest --tb=short
22
+ ```
23
+
24
+ ## Code Quality
25
+ ```bash
26
+ # Type checking (if mypy available)
27
+ mypy llm4agents --strict
28
+
29
+ # Linting (if ruff/flake8 available)
30
+ # Check project docs for configured linters
31
+ ```
32
+
33
+ ## Installation
34
+ ```bash
35
+ # Install dev dependencies
36
+ pip install -e ".[dev]"
37
+ ```
38
+
39
+ ## Git Workflow
40
+ ```bash
41
+ git add <files>
42
+ git commit -m "type: description"
43
+ git push origin <branch>
44
+ ```
@@ -0,0 +1,41 @@
1
+ # llm4agents-sdk-python Project Overview
2
+
3
+ ## Purpose
4
+ Python SDK for llm4agents.com — gasless AI agent infrastructure. Provides client library for interacting with the LLM4Agents API.
5
+
6
+ ## Tech Stack
7
+ - **Language:** Python 3.10+
8
+ - **HTTP:** httpx (async with HTTP/2)
9
+ - **Web3:** eth-account (for wallet signing)
10
+ - **Testing:** pytest, pytest-asyncio, respx (HTTP mocking)
11
+ - **Build:** hatchling
12
+
13
+ ## Architecture Overview
14
+ - **Entry Point:** `llm4agents/client.py` - Main LLM4AgentsClient facade
15
+ - **Transport:** `llm4agents/transport/` - HTTP and MCP transports
16
+ - **Modules:**
17
+ - `llm4agents/chat/` - Chat completions and conversations
18
+ - `llm4agents/wallets/` - Wallet management
19
+ - `llm4agents/transfer/` - Token transfers with signing
20
+ - `llm4agents/tools/` - MCP tool integrations (scraper, search, image)
21
+ - **Error Handling:** `llm4agents/errors.py` - Typed error codes and mapping
22
+
23
+ ## Module Structure
24
+ - All async-first design (httpx AsyncClient)
25
+ - Type hints throughout
26
+ - Namespace pattern for client submodules (e.g., client.chat, client.wallets)
27
+ - Conversation factory pattern for interactive chat
28
+ - Tool call support with callback hooks
29
+
30
+ ## Key Files to Know
31
+ - `llm4agents/__init__.py` - Package exports (currently empty)
32
+ - `llm4agents/types.py` - Type re-exports (to be created)
33
+ - `llm4agents/client.py` - Main client facade (to be created)
34
+ - Tests in `tests/` with structure matching source modules
35
+
36
+ ## Code Style
37
+ - Type hints required on all functions
38
+ - Use dataclass with frozen=True for immutable types
39
+ - Async/await for I/O operations
40
+ - Error handling via LLM4AgentsError with typed codes
41
+ - Namespace classes for logical grouping
@@ -0,0 +1,39 @@
1
+ # Task 9: Client Facade & Package Exports - COMPLETED
2
+
3
+ ## Files Created/Modified
4
+ 1. **llm4agents/client.py** - Main LLM4AgentsClient facade class
5
+ - Initializes HTTP and MCP transports
6
+ - Creates namespace objects (chat, wallets, transfer, tools, models)
7
+ - _ChatNamespace: completions property + conversation() factory method
8
+ - _ModelsNamespace: list() async method for models
9
+
10
+ 2. **llm4agents/types.py** - Re-export all public types
11
+ - Imports from errors, wallets.types, transfer.types, chat.types, chat.conversation
12
+ - Single source of truth for SDK public API types
13
+
14
+ 3. **llm4agents/__init__.py** - Package entry point
15
+ - Exports: LLM4AgentsClient, LLM4AgentsError, ErrorCode
16
+ - Clean public API surface
17
+
18
+ 4. **tests/test_client.py** - Client unit tests
19
+ - test_client_creates: checks all namespaces exist
20
+ - test_chat_namespace: verifies chat.completions and chat.conversation
21
+ - test_conversation_factory: confirms Conversation factory pattern works
22
+ - test_error_is_exported: verifies error is exported
23
+ - test_custom_base_url: tests custom base URL configuration
24
+
25
+ ## Test Results
26
+ - All 5 new tests pass
27
+ - Full test suite: 44/44 tests pass
28
+ - No regressions introduced
29
+
30
+ ## Commit
31
+ - Hash: f0bac1c
32
+ - Message: "feat: add client facade and package exports"
33
+
34
+ ## Architecture Notes
35
+ - Client uses dependency injection pattern: HttpTransport + McpTransport passed to submodules
36
+ - Namespace pattern for logical grouping (chat, wallets, transfer, tools, models)
37
+ - Conversation uses factory pattern via chat.conversation(opts)
38
+ - Default timeouts: 30s for HTTP, 60s for MCP
39
+ - Default base URLs are configurable at client init time
@@ -0,0 +1,5 @@
1
+ # This file allows you to locally override settings in project.yml for development purposes.
2
+ #
3
+ # Use the same keys as in project.yml here. Any setting you specify will override the corresponding
4
+ # setting in project.yml, allowing you to customise the configuration for your local development environment
5
+ # without affecting the project configuration in project.yml (which is intended to be versioned).
@@ -0,0 +1,127 @@
1
+ # the name by which the project can be referenced within Serena
2
+ project_name: "llm4agents-sdk-python"
3
+
4
+
5
+ # list of languages for which language servers are started; choose from:
6
+ # al ansible bash clojure cpp
7
+ # cpp_ccls crystal csharp csharp_omnisharp dart
8
+ # elixir elm erlang fortran fsharp
9
+ # go groovy haskell haxe hlsl
10
+ # java json julia kotlin lean4
11
+ # lua luau markdown matlab msl
12
+ # nix ocaml pascal perl php
13
+ # php_phpactor powershell python python_jedi python_ty
14
+ # r rego ruby ruby_solargraph rust
15
+ # scala solidity swift systemverilog terraform
16
+ # toml typescript typescript_vts vue yaml
17
+ # zig
18
+ # (This list may be outdated. For the current list, see values of Language enum here:
19
+ # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
20
+ # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
21
+ # Note:
22
+ # - For C, use cpp
23
+ # - For JavaScript, use typescript
24
+ # - For Free Pascal/Lazarus, use pascal
25
+ # Special requirements:
26
+ # Some languages require additional setup/installations.
27
+ # See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
28
+ # When using multiple languages, the first language server that supports a given file will be used for that file.
29
+ # The first language is the default language and the respective language server will be used as a fallback.
30
+ # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
31
+ languages:
32
+ - python
33
+
34
+ # the encoding used by text files in the project
35
+ # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
36
+ encoding: "utf-8"
37
+
38
+ # line ending convention to use when writing source files.
39
+ # Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
40
+ # This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
41
+ line_ending:
42
+
43
+ # The language backend to use for this project.
44
+ # If not set, the global setting from serena_config.yml is used.
45
+ # Valid values: LSP, JetBrains
46
+ # Note: the backend is fixed at startup. If a project with a different backend
47
+ # is activated post-init, an error will be returned.
48
+ language_backend:
49
+
50
+ # whether to use project's .gitignore files to ignore files
51
+ ignore_all_files_in_gitignore: true
52
+
53
+ # advanced configuration option allowing to configure language server-specific options.
54
+ # Maps the language key to the options.
55
+ # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
56
+ # No documentation on options means no options are available.
57
+ ls_specific_settings: {}
58
+
59
+ # list of additional paths to ignore in this project.
60
+ # Same syntax as gitignore, so you can use * and **.
61
+ # Note: global ignored_paths from serena_config.yml are also applied additively.
62
+ ignored_paths: []
63
+
64
+ # whether the project is in read-only mode
65
+ # If set to true, all editing tools will be disabled and attempts to use them will result in an error
66
+ # Added on 2025-04-18
67
+ read_only: false
68
+
69
+ # list of tool names to exclude.
70
+ # This extends the existing exclusions (e.g. from the global configuration)
71
+ # Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
72
+ excluded_tools: []
73
+
74
+ # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
75
+ # This extends the existing inclusions (e.g. from the global configuration).
76
+ # Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
77
+ included_optional_tools: []
78
+
79
+ # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
80
+ # This cannot be combined with non-empty excluded_tools or included_optional_tools.
81
+ # Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
82
+ fixed_tools: []
83
+
84
+ # list of mode names to that are always to be included in the set of active modes
85
+ # The full set of modes to be activated is base_modes + default_modes.
86
+ # If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
87
+ # Otherwise, this setting overrides the global configuration.
88
+ # Set this to [] to disable base modes for this project.
89
+ # Set this to a list of mode names to always include the respective modes for this project.
90
+ base_modes:
91
+
92
+ # list of mode names that are to be activated by default, overriding the setting in the global configuration.
93
+ # The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
94
+ # If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
95
+ # Otherwise, this overrides the setting from the global configuration (serena_config.yml).
96
+ # Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
97
+ # for this project.
98
+ # This setting can, in turn, be overridden by CLI parameters (--mode).
99
+ # See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
100
+ default_modes:
101
+
102
+ # initial prompt for the project. It will always be given to the LLM upon activating the project
103
+ # (contrary to the memories, which are loaded on demand).
104
+ initial_prompt: ""
105
+
106
+ # time budget (seconds) per tool call for the retrieval of additional symbol information
107
+ # such as docstrings or parameter information.
108
+ # This overrides the corresponding setting in the global configuration; see the documentation there.
109
+ # If null or missing, use the setting from the global configuration.
110
+ symbol_info_budget:
111
+
112
+ # list of regex patterns which, when matched, mark a memory entry as read‑only.
113
+ # Extends the list from the global configuration, merging the two lists.
114
+ read_only_memory_patterns: []
115
+
116
+ # list of regex patterns for memories to completely ignore.
117
+ # Matching memories will not appear in list_memories or activate_project output
118
+ # and cannot be accessed via read_memory or write_memory.
119
+ # To access ignored memory files, use the read_file tool on the raw file path.
120
+ # Extends the list from the global configuration, merging the two lists.
121
+ # Example: ["_archive/.*", "_episodes/.*"]
122
+ ignored_memory_patterns: []
123
+
124
+ # list of mode names to be activated additionally for this project, e.g. ["query-projects"]
125
+ # The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
126
+ # See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
127
+ added_modes:
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm4agents-sdk
3
+ Version: 2.0.0
4
+ Summary: Python SDK for llm4agents.com — gasless AI agent infrastructure
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: eth-account>=0.13.0
8
+ Requires-Dist: httpx[http2]>=0.27.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
12
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # llm4agents-sdk
16
+
17
+ Python SDK for [llm4agents.com](https://llm4agents.com) — gasless AI agent infrastructure.
18
+
19
+ ## Install
20
+
21
+ pip install llm4agents-sdk
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ import asyncio
27
+ from llm4agents import LLM4AgentsClient
28
+
29
+ async def main():
30
+ client = LLM4AgentsClient(api_key="sk-proxy-...")
31
+ conv = client.chat.conversation({"model": "openai/gpt-4o"})
32
+ result = await conv.say("Hello!")
33
+ print(result.content)
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ ## Version
39
+
40
+ This SDK tracks the TypeScript SDK. Version parity is maintained via CONTRACT.md in the TS SDK repo.
@@ -0,0 +1,26 @@
1
+ # llm4agents-sdk
2
+
3
+ Python SDK for [llm4agents.com](https://llm4agents.com) — gasless AI agent infrastructure.
4
+
5
+ ## Install
6
+
7
+ pip install llm4agents-sdk
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ import asyncio
13
+ from llm4agents import LLM4AgentsClient
14
+
15
+ async def main():
16
+ client = LLM4AgentsClient(api_key="sk-proxy-...")
17
+ conv = client.chat.conversation({"model": "openai/gpt-4o"})
18
+ result = await conv.say("Hello!")
19
+ print(result.content)
20
+
21
+ asyncio.run(main())
22
+ ```
23
+
24
+ ## Version
25
+
26
+ This SDK tracks the TypeScript SDK. Version parity is maintained via CONTRACT.md in the TS SDK repo.
@@ -0,0 +1,4 @@
1
+ from llm4agents.client import LLM4AgentsClient
2
+ from llm4agents.errors import LLM4AgentsError, ErrorCode
3
+
4
+ __all__ = ["LLM4AgentsClient", "LLM4AgentsError", "ErrorCode"]
File without changes
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+ from collections.abc import AsyncIterator
3
+ from typing import Any
4
+ from llm4agents.transport.http import HttpTransport
5
+ from llm4agents.chat.types import ChatResponse, StreamChunk
6
+
7
+
8
+ class ChatCompletions:
9
+ def __init__(self, http: HttpTransport) -> None:
10
+ self._http = http
11
+
12
+ async def create(
13
+ self, params: dict[str, Any]
14
+ ) -> ChatResponse | AsyncIterator[StreamChunk]:
15
+ if params.get("stream"):
16
+ raw_stream = await self._http.post_stream("/v1/chat/completions", params)
17
+ return self._wrap_stream(raw_stream)
18
+ data = await self._http.post("/v1/chat/completions", params)
19
+ return ChatResponse.from_dict(data)
20
+
21
+ async def _wrap_stream(
22
+ self, raw: AsyncIterator[Any]
23
+ ) -> AsyncIterator[StreamChunk]:
24
+ async for chunk in raw:
25
+ yield StreamChunk.from_dict(chunk)
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+ from llm4agents.transport.http import HttpTransport
6
+ from llm4agents.chat.types import ChatMessage
7
+ from llm4agents.errors import LLM4AgentsError
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ConversationResponse:
12
+ content: str
13
+ tool_calls: list[dict[str, Any]]
14
+ usage: dict[str, int]
15
+
16
+
17
+ class Conversation:
18
+ def __init__(self, http: HttpTransport, opts: dict[str, Any]) -> None:
19
+ self._http = http
20
+ self._model: str = opts["model"]
21
+ self._system: str | None = opts.get("system")
22
+ self._tools = opts.get("tools")
23
+ self._on_tool_call: Callable[[str, Any], bool] | None = opts.get("on_tool_call")
24
+ self._on_tool_result: Callable[[str, Any], None] | None = opts.get("on_tool_result")
25
+ self._max_tool_rounds: int = opts.get("max_tool_rounds", 10)
26
+ self._history: list[ChatMessage] = list(opts.get("history", []))
27
+ self._tool_rounds: int = 0
28
+
29
+ @property
30
+ def messages(self) -> list[ChatMessage]:
31
+ return list(self._history)
32
+
33
+ def clear(self) -> None:
34
+ self._history.clear()
35
+ self._tool_rounds = 0
36
+
37
+ def fork(self) -> Conversation:
38
+ opts: dict[str, Any] = {
39
+ "model": self._model,
40
+ "max_tool_rounds": self._max_tool_rounds,
41
+ "history": list(self._history),
42
+ }
43
+ if self._system is not None:
44
+ opts["system"] = self._system
45
+ if self._tools is not None:
46
+ opts["tools"] = self._tools
47
+ if self._on_tool_call is not None:
48
+ opts["on_tool_call"] = self._on_tool_call
49
+ if self._on_tool_result is not None:
50
+ opts["on_tool_result"] = self._on_tool_result
51
+ return Conversation(self._http, opts)
52
+
53
+ def _check_tool_limit(self) -> None:
54
+ if self._tool_rounds >= self._max_tool_rounds:
55
+ raise LLM4AgentsError(
56
+ f"Tool loop limit reached ({self._max_tool_rounds})",
57
+ "tool_loop_limit",
58
+ None,
59
+ None,
60
+ )
61
+
62
+ async def say(self, message: str) -> ConversationResponse:
63
+ self._history.append(
64
+ {"role": "user", "content": message, "tool_calls": None, "tool_call_id": None}
65
+ )
66
+
67
+ all_tool_calls: list[dict[str, Any]] = []
68
+ final_usage: dict[str, int] = {}
69
+
70
+ while True:
71
+ params: dict[str, Any] = {
72
+ "model": self._model,
73
+ "messages": self._build_messages(),
74
+ }
75
+ if self._tools is not None:
76
+ if not self._tools.definitions:
77
+ await self._tools.fetch_definitions()
78
+ params["tools"] = self._tools.definitions
79
+
80
+ data = await self._http.post("/v1/chat/completions", params)
81
+ choice = data["choices"][0]
82
+ msg: ChatMessage = choice["message"]
83
+ usage: dict[str, int] = data.get("usage") or {}
84
+ final_usage = usage
85
+ finish_reason: str = choice.get("finish_reason", "stop")
86
+
87
+ self._history.append(msg)
88
+
89
+ raw_tool_calls: list[dict[str, Any]] = msg.get("tool_calls") or []
90
+
91
+ if not raw_tool_calls or finish_reason == "stop":
92
+ return ConversationResponse(
93
+ content=msg.get("content") or "",
94
+ tool_calls=all_tool_calls,
95
+ usage=final_usage,
96
+ )
97
+
98
+ self._check_tool_limit()
99
+ self._tool_rounds += 1
100
+
101
+ for tc in raw_tool_calls:
102
+ fn = tc["function"]
103
+ name: str = fn["name"]
104
+ try:
105
+ args = json.loads(fn.get("arguments") or "{}")
106
+ except json.JSONDecodeError:
107
+ args = {}
108
+
109
+ if self._on_tool_call is not None:
110
+ should_continue = self._on_tool_call(name, args)
111
+ if not should_continue:
112
+ return ConversationResponse(
113
+ content="",
114
+ tool_calls=all_tool_calls,
115
+ usage=final_usage,
116
+ )
117
+
118
+ result = await self._tools.call(name, args)
119
+
120
+ if self._on_tool_result is not None:
121
+ self._on_tool_result(name, result)
122
+
123
+ all_tool_calls.append({"name": name, "args": args, "result": result})
124
+ self._history.append({
125
+ "role": "tool",
126
+ "content": result.text,
127
+ "tool_calls": None,
128
+ "tool_call_id": tc.get("id"),
129
+ })
130
+
131
+ def _build_messages(self) -> list[ChatMessage]:
132
+ if self._system:
133
+ return [
134
+ {"role": "system", "content": self._system, "tool_calls": None, "tool_call_id": None},
135
+ *self._history,
136
+ ]
137
+ return list(self._history)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any, TypedDict
4
+
5
+
6
+ class ChatMessage(TypedDict, total=False):
7
+ role: str
8
+ content: str | None
9
+ tool_calls: list[Any] | None
10
+ tool_call_id: str | None
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ChatResponse:
15
+ id: str
16
+ choices: tuple[dict[str, Any], ...]
17
+ usage: dict[str, int]
18
+ model: str
19
+
20
+ @classmethod
21
+ def from_dict(cls, d: dict[str, Any]) -> ChatResponse:
22
+ return cls(
23
+ id=d["id"],
24
+ choices=tuple(d.get("choices", [])),
25
+ usage=d.get("usage") or {},
26
+ model=d.get("model", ""),
27
+ )
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class StreamChunk:
32
+ id: str
33
+ choices: tuple[dict[str, Any], ...]
34
+ usage: dict[str, int] | None
35
+ model: str | None
36
+
37
+ @classmethod
38
+ def from_dict(cls, d: dict[str, Any]) -> StreamChunk:
39
+ return cls(
40
+ id=d["id"],
41
+ choices=tuple(d.get("choices", [])),
42
+ usage=d.get("usage"),
43
+ model=d.get("model"),
44
+ )
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+ from llm4agents.transport.http import HttpTransport
5
+ from llm4agents.transport.mcp import McpTransport
6
+ from llm4agents.chat.completions import ChatCompletions
7
+ from llm4agents.chat.conversation import Conversation
8
+ from llm4agents.wallets.wallets import Wallets
9
+ from llm4agents.transfer.transfer import Transfer
10
+ from llm4agents.tools.tools import Tools
11
+
12
+ _DEFAULT_BASE_URL = "https://api.llm4agents.com"
13
+ _DEFAULT_MCP_URL = "https://mcp.llm4agents.com/mcp"
14
+ _DEFAULT_TIMEOUT = 30.0
15
+ _MCP_TIMEOUT = 60.0
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ModelListResult:
20
+ models: list[dict[str, Any]]
21
+ request_id: str | None
22
+
23
+
24
+ class _ChatNamespace:
25
+ def __init__(self, completions: ChatCompletions, http: HttpTransport) -> None:
26
+ self.completions = completions
27
+ self._http = http
28
+
29
+ def conversation(self, opts: dict[str, Any]) -> Conversation:
30
+ return Conversation(self._http, opts)
31
+
32
+
33
+ class _ModelsNamespace:
34
+ def __init__(self, http: HttpTransport) -> None:
35
+ self._http = http
36
+
37
+ async def list(self, search: str | None = None) -> ModelListResult:
38
+ params: dict[str, str] | None = {"search": search} if search else None
39
+ data = await self._http.get("/api/v1/models/", params=params)
40
+ return ModelListResult(
41
+ models=data.get("models", []),
42
+ request_id=data.get("requestId"),
43
+ )
44
+
45
+
46
+ class LLM4AgentsClient:
47
+ def __init__(
48
+ self,
49
+ *,
50
+ api_key: str,
51
+ base_url: str = _DEFAULT_BASE_URL,
52
+ mcp_url: str = _DEFAULT_MCP_URL,
53
+ timeout: float = _DEFAULT_TIMEOUT,
54
+ ) -> None:
55
+ self._http = HttpTransport(base_url, api_key, timeout)
56
+ mcp = McpTransport(mcp_url, api_key, _MCP_TIMEOUT)
57
+
58
+ completions = ChatCompletions(self._http)
59
+ self.chat = _ChatNamespace(completions, self._http)
60
+ self.wallets = Wallets(self._http)
61
+ self.transfer = Transfer(self._http)
62
+ self.tools = Tools(mcp)
63
+ self.models = _ModelsNamespace(self._http)