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.
- llm4agents_sdk-2.0.0/.github/workflows/publish.yml +23 -0
- llm4agents_sdk-2.0.0/.gitignore +8 -0
- llm4agents_sdk-2.0.0/.serena/.gitignore +2 -0
- llm4agents_sdk-2.0.0/.serena/memories/project/commands.md +44 -0
- llm4agents_sdk-2.0.0/.serena/memories/project/overview.md +41 -0
- llm4agents_sdk-2.0.0/.serena/memories/tasks/task-9-client-implementation.md +39 -0
- llm4agents_sdk-2.0.0/.serena/project.local.yml +5 -0
- llm4agents_sdk-2.0.0/.serena/project.yml +127 -0
- llm4agents_sdk-2.0.0/PKG-INFO +40 -0
- llm4agents_sdk-2.0.0/README.md +26 -0
- llm4agents_sdk-2.0.0/llm4agents/__init__.py +4 -0
- llm4agents_sdk-2.0.0/llm4agents/chat/__init__.py +0 -0
- llm4agents_sdk-2.0.0/llm4agents/chat/completions.py +25 -0
- llm4agents_sdk-2.0.0/llm4agents/chat/conversation.py +137 -0
- llm4agents_sdk-2.0.0/llm4agents/chat/types.py +44 -0
- llm4agents_sdk-2.0.0/llm4agents/client.py +63 -0
- llm4agents_sdk-2.0.0/llm4agents/errors.py +81 -0
- llm4agents_sdk-2.0.0/llm4agents/tools/__init__.py +4 -0
- llm4agents_sdk-2.0.0/llm4agents/tools/tools.py +99 -0
- llm4agents_sdk-2.0.0/llm4agents/tools/types.py +42 -0
- llm4agents_sdk-2.0.0/llm4agents/transfer/__init__.py +0 -0
- llm4agents_sdk-2.0.0/llm4agents/transfer/signer.py +87 -0
- llm4agents_sdk-2.0.0/llm4agents/transfer/transfer.py +47 -0
- llm4agents_sdk-2.0.0/llm4agents/transfer/types.py +69 -0
- llm4agents_sdk-2.0.0/llm4agents/transport/__init__.py +0 -0
- llm4agents_sdk-2.0.0/llm4agents/transport/http.py +85 -0
- llm4agents_sdk-2.0.0/llm4agents/transport/mcp.py +89 -0
- llm4agents_sdk-2.0.0/llm4agents/types.py +38 -0
- llm4agents_sdk-2.0.0/llm4agents/wallets/__init__.py +11 -0
- llm4agents_sdk-2.0.0/llm4agents/wallets/types.py +102 -0
- llm4agents_sdk-2.0.0/llm4agents/wallets/wallets.py +27 -0
- llm4agents_sdk-2.0.0/pyproject.toml +29 -0
- llm4agents_sdk-2.0.0/tests/__init__.py +0 -0
- llm4agents_sdk-2.0.0/tests/chat/__init__.py +0 -0
- llm4agents_sdk-2.0.0/tests/chat/test_completions.py +55 -0
- llm4agents_sdk-2.0.0/tests/chat/test_conversation.py +113 -0
- llm4agents_sdk-2.0.0/tests/test_client.py +38 -0
- llm4agents_sdk-2.0.0/tests/test_errors.py +34 -0
- llm4agents_sdk-2.0.0/tests/test_tools.py +145 -0
- llm4agents_sdk-2.0.0/tests/test_transfer.py +132 -0
- llm4agents_sdk-2.0.0/tests/test_wallets.py +83 -0
- llm4agents_sdk-2.0.0/tests/transport/__init__.py +0 -0
- llm4agents_sdk-2.0.0/tests/transport/test_http.py +39 -0
- 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,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.
|
|
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)
|