llm4agents-sdk 1.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-1.0.0/.github/workflows/publish.yml +23 -0
  2. llm4agents_sdk-1.0.0/.gitignore +8 -0
  3. llm4agents_sdk-1.0.0/.serena/.gitignore +2 -0
  4. llm4agents_sdk-1.0.0/.serena/memories/project/commands.md +44 -0
  5. llm4agents_sdk-1.0.0/.serena/memories/project/overview.md +41 -0
  6. llm4agents_sdk-1.0.0/.serena/memories/tasks/task-9-client-implementation.md +39 -0
  7. llm4agents_sdk-1.0.0/.serena/project.local.yml +5 -0
  8. llm4agents_sdk-1.0.0/.serena/project.yml +154 -0
  9. llm4agents_sdk-1.0.0/PKG-INFO +40 -0
  10. llm4agents_sdk-1.0.0/README.md +26 -0
  11. llm4agents_sdk-1.0.0/llm4agents/__init__.py +4 -0
  12. llm4agents_sdk-1.0.0/llm4agents/chat/__init__.py +0 -0
  13. llm4agents_sdk-1.0.0/llm4agents/chat/completions.py +25 -0
  14. llm4agents_sdk-1.0.0/llm4agents/chat/conversation.py +137 -0
  15. llm4agents_sdk-1.0.0/llm4agents/chat/types.py +44 -0
  16. llm4agents_sdk-1.0.0/llm4agents/client.py +52 -0
  17. llm4agents_sdk-1.0.0/llm4agents/errors.py +81 -0
  18. llm4agents_sdk-1.0.0/llm4agents/tools/__init__.py +4 -0
  19. llm4agents_sdk-1.0.0/llm4agents/tools/tools.py +87 -0
  20. llm4agents_sdk-1.0.0/llm4agents/tools/types.py +7 -0
  21. llm4agents_sdk-1.0.0/llm4agents/transfer/__init__.py +0 -0
  22. llm4agents_sdk-1.0.0/llm4agents/transfer/signer.py +87 -0
  23. llm4agents_sdk-1.0.0/llm4agents/transfer/transfer.py +47 -0
  24. llm4agents_sdk-1.0.0/llm4agents/transfer/types.py +69 -0
  25. llm4agents_sdk-1.0.0/llm4agents/transport/__init__.py +0 -0
  26. llm4agents_sdk-1.0.0/llm4agents/transport/http.py +85 -0
  27. llm4agents_sdk-1.0.0/llm4agents/transport/mcp.py +64 -0
  28. llm4agents_sdk-1.0.0/llm4agents/types.py +21 -0
  29. llm4agents_sdk-1.0.0/llm4agents/wallets/__init__.py +11 -0
  30. llm4agents_sdk-1.0.0/llm4agents/wallets/types.py +102 -0
  31. llm4agents_sdk-1.0.0/llm4agents/wallets/wallets.py +27 -0
  32. llm4agents_sdk-1.0.0/pyproject.toml +29 -0
  33. llm4agents_sdk-1.0.0/tests/__init__.py +0 -0
  34. llm4agents_sdk-1.0.0/tests/chat/__init__.py +0 -0
  35. llm4agents_sdk-1.0.0/tests/chat/test_completions.py +55 -0
  36. llm4agents_sdk-1.0.0/tests/chat/test_conversation.py +109 -0
  37. llm4agents_sdk-1.0.0/tests/test_client.py +38 -0
  38. llm4agents_sdk-1.0.0/tests/test_errors.py +34 -0
  39. llm4agents_sdk-1.0.0/tests/test_tools.py +245 -0
  40. llm4agents_sdk-1.0.0/tests/test_transfer.py +132 -0
  41. llm4agents_sdk-1.0.0/tests/test_wallets.py +83 -0
  42. llm4agents_sdk-1.0.0/tests/transport/__init__.py +0 -0
  43. llm4agents_sdk-1.0.0/tests/transport/test_http.py +39 -0
  44. llm4agents_sdk-1.0.0/tests/transport/test_mcp.py +55 -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,154 @@
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 bash clojure cpp csharp
7
+ # csharp_omnisharp dart elixir elm erlang
8
+ # fortran fsharp go groovy haskell
9
+ # haxe java julia kotlin lua
10
+ # markdown
11
+ # matlab nix pascal perl php
12
+ # php_phpactor powershell python python_jedi r
13
+ # rego ruby ruby_solargraph rust scala
14
+ # swift terraform toml typescript typescript_vts
15
+ # vue yaml zig
16
+ # (This list may be outdated. For the current list, see values of Language enum here:
17
+ # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
18
+ # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
19
+ # Note:
20
+ # - For C, use cpp
21
+ # - For JavaScript, use typescript
22
+ # - For Free Pascal/Lazarus, use pascal
23
+ # Special requirements:
24
+ # Some languages require additional setup/installations.
25
+ # See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
26
+ # When using multiple languages, the first language server that supports a given file will be used for that file.
27
+ # The first language is the default language and the respective language server will be used as a fallback.
28
+ # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
29
+ languages:
30
+ - python
31
+
32
+ # the encoding used by text files in the project
33
+ # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
34
+ encoding: "utf-8"
35
+
36
+ # line ending convention to use when writing source files.
37
+ # Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
38
+ # This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
39
+ line_ending:
40
+
41
+ # The language backend to use for this project.
42
+ # If not set, the global setting from serena_config.yml is used.
43
+ # Valid values: LSP, JetBrains
44
+ # Note: the backend is fixed at startup. If a project with a different backend
45
+ # is activated post-init, an error will be returned.
46
+ language_backend:
47
+
48
+ # whether to use project's .gitignore files to ignore files
49
+ ignore_all_files_in_gitignore: true
50
+
51
+ # advanced configuration option allowing to configure language server-specific options.
52
+ # Maps the language key to the options.
53
+ # 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.
54
+ # No documentation on options means no options are available.
55
+ ls_specific_settings: {}
56
+
57
+ # list of additional paths to ignore in this project.
58
+ # Same syntax as gitignore, so you can use * and **.
59
+ # Note: global ignored_paths from serena_config.yml are also applied additively.
60
+ ignored_paths: []
61
+
62
+ # whether the project is in read-only mode
63
+ # If set to true, all editing tools will be disabled and attempts to use them will result in an error
64
+ # Added on 2025-04-18
65
+ read_only: false
66
+
67
+ # list of tool names to exclude.
68
+ # This extends the existing exclusions (e.g. from the global configuration)
69
+ #
70
+ # Below is the complete list of tools for convenience.
71
+ # To make sure you have the latest list of tools, and to view their descriptions,
72
+ # execute `uv run scripts/print_tool_overview.py`.
73
+ #
74
+ # * `activate_project`: Activates a project based on the project name or path.
75
+ # * `check_onboarding_performed`: Checks whether project onboarding was already performed.
76
+ # * `create_text_file`: Creates/overwrites a file in the project directory.
77
+ # * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
78
+ # for example by saying that the information retrieved from a memory file is no longer correct
79
+ # or no longer relevant for the project.
80
+ # * `edit_memory`: Replaces content matching a regular expression in a memory.
81
+ # * `execute_shell_command`: Executes a shell command.
82
+ # * `find_file`: Finds files in the given relative paths
83
+ # * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
84
+ # * `find_symbol`: Performs a global (or local) search using the language server backend.
85
+ # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
86
+ # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
87
+ # * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
88
+ # for clients that do not read the initial instructions when the MCP server is connected.
89
+ # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
90
+ # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
91
+ # * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
92
+ # * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
93
+ # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
94
+ # * `read_file`: Reads a file within the project directory.
95
+ # * `read_memory`: Read the content of a memory file. This tool should only be used if the information
96
+ # is relevant to the current task. You can infer whether the information
97
+ # is relevant from the memory file name.
98
+ # You should not read the same memory file multiple times in the same conversation.
99
+ # * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
100
+ # (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
101
+ # * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
102
+ # For JB, we use a separate tool.
103
+ # * `replace_content`: Replaces content in a file (optionally using regular expressions).
104
+ # * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
105
+ # * `safe_delete_symbol`:
106
+ # * `search_for_pattern`: Performs a search for a pattern in the project.
107
+ # * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
108
+ # The memory name should be meaningful.
109
+ excluded_tools: []
110
+
111
+ # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
112
+ # This extends the existing inclusions (e.g. from the global configuration).
113
+ included_optional_tools: []
114
+
115
+ # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
116
+ # This cannot be combined with non-empty excluded_tools or included_optional_tools.
117
+ fixed_tools: []
118
+
119
+ # list of mode names to that are always to be included in the set of active modes
120
+ # The full set of modes to be activated is base_modes + default_modes.
121
+ # If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
122
+ # Otherwise, this setting overrides the global configuration.
123
+ # Set this to [] to disable base modes for this project.
124
+ # Set this to a list of mode names to always include the respective modes for this project.
125
+ base_modes:
126
+
127
+ # list of mode names that are to be activated by default.
128
+ # The full set of modes to be activated is base_modes + default_modes.
129
+ # If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
130
+ # Otherwise, this overrides the setting from the global configuration (serena_config.yml).
131
+ # This setting can, in turn, be overridden by CLI parameters (--mode).
132
+ default_modes:
133
+
134
+ # initial prompt for the project. It will always be given to the LLM upon activating the project
135
+ # (contrary to the memories, which are loaded on demand).
136
+ initial_prompt: ""
137
+
138
+ # time budget (seconds) per tool call for the retrieval of additional symbol information
139
+ # such as docstrings or parameter information.
140
+ # This overrides the corresponding setting in the global configuration; see the documentation there.
141
+ # If null or missing, use the setting from the global configuration.
142
+ symbol_info_budget:
143
+
144
+ # list of regex patterns which, when matched, mark a memory entry as read‑only.
145
+ # Extends the list from the global configuration, merging the two lists.
146
+ read_only_memory_patterns: []
147
+
148
+ # list of regex patterns for memories to completely ignore.
149
+ # Matching memories will not appear in list_memories or activate_project output
150
+ # and cannot be accessed via read_memory or write_memory.
151
+ # To access ignored memory files, use the read_file tool on the raw file path.
152
+ # Extends the list from the global configuration, merging the two lists.
153
+ # Example: ["_archive/.*", "_episodes/.*"]
154
+ ignored_memory_patterns: []
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm4agents-sdk
3
+ Version: 1.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": str(result),
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,52 @@
1
+ from __future__ import annotations
2
+ from typing import Any
3
+ from llm4agents.transport.http import HttpTransport
4
+ from llm4agents.transport.mcp import McpTransport
5
+ from llm4agents.chat.completions import ChatCompletions
6
+ from llm4agents.chat.conversation import Conversation
7
+ from llm4agents.wallets.wallets import Wallets
8
+ from llm4agents.transfer.transfer import Transfer
9
+ from llm4agents.tools.tools import Tools
10
+
11
+ _DEFAULT_BASE_URL = "https://api.llm4agents.com"
12
+ _DEFAULT_MCP_URL = "https://mcp.llm4agents.com/mcp"
13
+ _DEFAULT_TIMEOUT = 30.0
14
+ _MCP_TIMEOUT = 60.0
15
+
16
+
17
+ class _ChatNamespace:
18
+ def __init__(self, completions: ChatCompletions, http: HttpTransport) -> None:
19
+ self.completions = completions
20
+ self._http = http
21
+
22
+ def conversation(self, opts: dict[str, Any]) -> Conversation:
23
+ return Conversation(self._http, opts)
24
+
25
+
26
+ class _ModelsNamespace:
27
+ def __init__(self, http: HttpTransport) -> None:
28
+ self._http = http
29
+
30
+ async def list(self) -> list[dict[str, Any]]:
31
+ data = await self._http.get("/api/v1/models/")
32
+ return data.get("models", [])
33
+
34
+
35
+ class LLM4AgentsClient:
36
+ def __init__(
37
+ self,
38
+ *,
39
+ api_key: str,
40
+ base_url: str = _DEFAULT_BASE_URL,
41
+ mcp_url: str = _DEFAULT_MCP_URL,
42
+ timeout: float = _DEFAULT_TIMEOUT,
43
+ ) -> None:
44
+ self._http = HttpTransport(base_url, api_key, timeout)
45
+ mcp = McpTransport(mcp_url, api_key, _MCP_TIMEOUT)
46
+
47
+ completions = ChatCompletions(self._http)
48
+ self.chat = _ChatNamespace(completions, self._http)
49
+ self.wallets = Wallets(self._http)
50
+ self.transfer = Transfer(self._http)
51
+ self.tools = Tools(mcp)
52
+ self.models = _ModelsNamespace(self._http)