dacp 0.1.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.
dacp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 DACP Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dacp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: dacp
3
+ Version: 0.1.0
4
+ Summary: DACP - Declarative Agent Communication Protocol for LLM/agent communications and tool function calls
5
+ Author-email: Your Name <your.email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/dacp
8
+ Project-URL: Repository, https://github.com/yourusername/dacp
9
+ Project-URL: Issues, https://github.com/yourusername/dacp/issues
10
+ Keywords: llm,agent,tools,openai,communication
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: openai>=1.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
27
+ Requires-Dist: black>=22.0.0; extra == "dev"
28
+ Requires-Dist: flake8>=5.0.0; extra == "dev"
29
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
30
+ Requires-Dist: types-requests>=2.28.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # DACP - Delcarative Agent Communication Protocol
34
+
35
+ A Python library for managing LLM/agent communications and tool function calls following the OAS Open Agent Specification.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install -e .
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ import dacp
47
+
48
+ # Register a custom tool
49
+ def my_custom_tool(param1: str, param2: int) -> dict:
50
+ return {"result": f"Processed {param1} with {param2}"}
51
+
52
+ dacp.register_tool("my_custom_tool", my_custom_tool)
53
+
54
+ # Call an LLM
55
+ response = dacp.call_llm("What is the weather like today?")
56
+
57
+ # Parse agent response
58
+ parsed = dacp.parse_agent_response(response)
59
+
60
+ # Check if it's a tool request
61
+ if dacp.is_tool_request(parsed):
62
+ tool_name, args = dacp.get_tool_request(parsed)
63
+ result = dacp.run_tool(tool_name, args)
64
+ tool_response = dacp.wrap_tool_result(tool_name, result)
65
+ ```
66
+
67
+ ## Features
68
+
69
+ - **Tool Registry**: Register and manage custom tools for LLM agents
70
+ - **LLM Integration**: Built-in support for OpenAI models (extensible)
71
+ - **Protocol Parsing**: Parse and validate agent responses
72
+ - **Tool Execution**: Safe execution of registered tools
73
+ - **OAS Compliance**: Follows Open Agent Specification standards
74
+
75
+ ## API Reference
76
+
77
+ ### Tools
78
+
79
+ - `register_tool(tool_id: str, func)`: Register a new tool
80
+ - `run_tool(tool_id: str, args: Dict) -> dict`: Execute a registered tool
81
+ - `TOOL_REGISTRY`: Access the current tool registry
82
+
83
+ ### LLM
84
+
85
+ - `call_llm(prompt: str, model: str = "gpt-4") -> str`: Call an LLM with a prompt
86
+
87
+ ### Protocol
88
+
89
+ - `parse_agent_response(response: str | dict) -> dict`: Parse agent response
90
+ - `is_tool_request(msg: dict) -> bool`: Check if message is a tool request
91
+ - `get_tool_request(msg: dict) -> tuple[str, dict]`: Extract tool request details
92
+ - `wrap_tool_result(name: str, result: dict) -> dict`: Wrap tool result for agent
93
+ - `is_final_response(msg: dict) -> bool`: Check if message is a final response
94
+ - `get_final_response(msg: dict) -> dict`: Extract final response
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ # Install development dependencies
100
+ pip install -e .[dev]
101
+
102
+ # Run tests
103
+ pytest
104
+
105
+ # Format code
106
+ black .
107
+
108
+ # Lint code
109
+ flake8
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT License
dacp-0.1.0/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # DACP - Delcarative Agent Communication Protocol
2
+
3
+ A Python library for managing LLM/agent communications and tool function calls following the OAS Open Agent Specification.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import dacp
15
+
16
+ # Register a custom tool
17
+ def my_custom_tool(param1: str, param2: int) -> dict:
18
+ return {"result": f"Processed {param1} with {param2}"}
19
+
20
+ dacp.register_tool("my_custom_tool", my_custom_tool)
21
+
22
+ # Call an LLM
23
+ response = dacp.call_llm("What is the weather like today?")
24
+
25
+ # Parse agent response
26
+ parsed = dacp.parse_agent_response(response)
27
+
28
+ # Check if it's a tool request
29
+ if dacp.is_tool_request(parsed):
30
+ tool_name, args = dacp.get_tool_request(parsed)
31
+ result = dacp.run_tool(tool_name, args)
32
+ tool_response = dacp.wrap_tool_result(tool_name, result)
33
+ ```
34
+
35
+ ## Features
36
+
37
+ - **Tool Registry**: Register and manage custom tools for LLM agents
38
+ - **LLM Integration**: Built-in support for OpenAI models (extensible)
39
+ - **Protocol Parsing**: Parse and validate agent responses
40
+ - **Tool Execution**: Safe execution of registered tools
41
+ - **OAS Compliance**: Follows Open Agent Specification standards
42
+
43
+ ## API Reference
44
+
45
+ ### Tools
46
+
47
+ - `register_tool(tool_id: str, func)`: Register a new tool
48
+ - `run_tool(tool_id: str, args: Dict) -> dict`: Execute a registered tool
49
+ - `TOOL_REGISTRY`: Access the current tool registry
50
+
51
+ ### LLM
52
+
53
+ - `call_llm(prompt: str, model: str = "gpt-4") -> str`: Call an LLM with a prompt
54
+
55
+ ### Protocol
56
+
57
+ - `parse_agent_response(response: str | dict) -> dict`: Parse agent response
58
+ - `is_tool_request(msg: dict) -> bool`: Check if message is a tool request
59
+ - `get_tool_request(msg: dict) -> tuple[str, dict]`: Extract tool request details
60
+ - `wrap_tool_result(name: str, result: dict) -> dict`: Wrap tool result for agent
61
+ - `is_final_response(msg: dict) -> bool`: Check if message is a final response
62
+ - `get_final_response(msg: dict) -> dict`: Extract final response
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ # Install development dependencies
68
+ pip install -e .[dev]
69
+
70
+ # Run tests
71
+ pytest
72
+
73
+ # Format code
74
+ black .
75
+
76
+ # Lint code
77
+ flake8
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT License
@@ -0,0 +1,31 @@
1
+ """
2
+ DACP - Declarative Agent Communication Protocol
3
+
4
+ A Python library for managing LLM/agent communications and tool function calls
5
+ following the OAS Open Agent Specification.
6
+ """
7
+
8
+ from .tools import register_tool, run_tool, TOOL_REGISTRY
9
+ from .llm import call_llm
10
+ from .protocol import (
11
+ parse_agent_response,
12
+ is_tool_request,
13
+ get_tool_request,
14
+ wrap_tool_result,
15
+ is_final_response,
16
+ get_final_response,
17
+ )
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = [
21
+ "register_tool",
22
+ "run_tool",
23
+ "TOOL_REGISTRY",
24
+ "call_llm",
25
+ "parse_agent_response",
26
+ "is_tool_request",
27
+ "get_tool_request",
28
+ "wrap_tool_result",
29
+ "is_final_response",
30
+ "get_final_response",
31
+ ]
File without changes
dacp-0.1.0/dacp/llm.py ADDED
@@ -0,0 +1,19 @@
1
+ import os
2
+ import openai
3
+
4
+
5
+ def call_llm(prompt: str, model: str = "gpt-4") -> str:
6
+ # Default: OpenAI, can later extend for other LLMs
7
+ client = openai.OpenAI(
8
+ api_key=os.getenv("OPENAI_API_KEY"), base_url="https://api.openai.com/v1"
9
+ )
10
+ response = client.chat.completions.create(
11
+ model=model,
12
+ messages=[{"role": "user", "content": prompt}],
13
+ temperature=0.7,
14
+ max_tokens=150,
15
+ )
16
+ content = response.choices[0].message.content
17
+ if content is None:
18
+ raise ValueError("LLM returned empty response")
19
+ return content
@@ -0,0 +1,35 @@
1
+ import json
2
+ from typing import Dict, Any, Union, Tuple, cast
3
+
4
+
5
+ def parse_agent_response(response: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
6
+ """
7
+ Parse the agent/LLM response (as string or dict) and return a dict.
8
+ """
9
+ if isinstance(response, dict):
10
+ return response
11
+ try:
12
+ return cast(Dict[str, Any], json.loads(response))
13
+ except Exception as e:
14
+ raise ValueError(f"Malformed agent response: {e}")
15
+
16
+
17
+ def is_tool_request(msg: Dict[str, Any]) -> bool:
18
+ return "tool_request" in msg
19
+
20
+
21
+ def get_tool_request(msg: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
22
+ req = msg["tool_request"]
23
+ return req["name"], req.get("args", {})
24
+
25
+
26
+ def wrap_tool_result(name: str, result: Dict[str, Any]) -> Dict[str, Any]:
27
+ return {"tool_result": {"name": name, "result": result}}
28
+
29
+
30
+ def is_final_response(msg: Dict[str, Any]) -> bool:
31
+ return "final_response" in msg
32
+
33
+
34
+ def get_final_response(msg: Dict[str, Any]) -> Dict[str, Any]:
35
+ return cast(Dict[str, Any], msg["final_response"])
@@ -0,0 +1,30 @@
1
+ from typing import Dict, Any, Callable
2
+
3
+ TOOL_REGISTRY: Dict[str, Callable[..., Dict[str, Any]]] = {}
4
+
5
+
6
+ def register_tool(tool_id: str, func: Callable[..., Dict[str, Any]]) -> None:
7
+ """Register a tool function."""
8
+ TOOL_REGISTRY[tool_id] = func
9
+
10
+
11
+ def run_tool(tool_id: str, args: Dict[str, Any]) -> Dict[str, Any]:
12
+ """Run a registered tool with the given arguments."""
13
+ if tool_id not in TOOL_REGISTRY:
14
+ raise ValueError(f"Unknown tool: {tool_id}")
15
+ tool_func = TOOL_REGISTRY[tool_id]
16
+ return tool_func(**args)
17
+
18
+
19
+ # --- Example tool ---
20
+ def file_writer(path: str, content: str) -> dict:
21
+ # Only allow paths in /tmp or ./output for now!
22
+ allowed_prefixes = ["./output/", "/tmp/"]
23
+ if not any(path.startswith(prefix) for prefix in allowed_prefixes):
24
+ raise ValueError("Path not allowed")
25
+ with open(path, "w") as f:
26
+ f.write(content)
27
+ return {"result": f"Written to {path}"}
28
+
29
+
30
+ register_tool("file_writer", file_writer)
File without changes
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: dacp
3
+ Version: 0.1.0
4
+ Summary: DACP - Declarative Agent Communication Protocol for LLM/agent communications and tool function calls
5
+ Author-email: Your Name <your.email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/dacp
8
+ Project-URL: Repository, https://github.com/yourusername/dacp
9
+ Project-URL: Issues, https://github.com/yourusername/dacp/issues
10
+ Keywords: llm,agent,tools,openai,communication
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: openai>=1.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
27
+ Requires-Dist: black>=22.0.0; extra == "dev"
28
+ Requires-Dist: flake8>=5.0.0; extra == "dev"
29
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
30
+ Requires-Dist: types-requests>=2.28.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # DACP - Delcarative Agent Communication Protocol
34
+
35
+ A Python library for managing LLM/agent communications and tool function calls following the OAS Open Agent Specification.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install -e .
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ import dacp
47
+
48
+ # Register a custom tool
49
+ def my_custom_tool(param1: str, param2: int) -> dict:
50
+ return {"result": f"Processed {param1} with {param2}"}
51
+
52
+ dacp.register_tool("my_custom_tool", my_custom_tool)
53
+
54
+ # Call an LLM
55
+ response = dacp.call_llm("What is the weather like today?")
56
+
57
+ # Parse agent response
58
+ parsed = dacp.parse_agent_response(response)
59
+
60
+ # Check if it's a tool request
61
+ if dacp.is_tool_request(parsed):
62
+ tool_name, args = dacp.get_tool_request(parsed)
63
+ result = dacp.run_tool(tool_name, args)
64
+ tool_response = dacp.wrap_tool_result(tool_name, result)
65
+ ```
66
+
67
+ ## Features
68
+
69
+ - **Tool Registry**: Register and manage custom tools for LLM agents
70
+ - **LLM Integration**: Built-in support for OpenAI models (extensible)
71
+ - **Protocol Parsing**: Parse and validate agent responses
72
+ - **Tool Execution**: Safe execution of registered tools
73
+ - **OAS Compliance**: Follows Open Agent Specification standards
74
+
75
+ ## API Reference
76
+
77
+ ### Tools
78
+
79
+ - `register_tool(tool_id: str, func)`: Register a new tool
80
+ - `run_tool(tool_id: str, args: Dict) -> dict`: Execute a registered tool
81
+ - `TOOL_REGISTRY`: Access the current tool registry
82
+
83
+ ### LLM
84
+
85
+ - `call_llm(prompt: str, model: str = "gpt-4") -> str`: Call an LLM with a prompt
86
+
87
+ ### Protocol
88
+
89
+ - `parse_agent_response(response: str | dict) -> dict`: Parse agent response
90
+ - `is_tool_request(msg: dict) -> bool`: Check if message is a tool request
91
+ - `get_tool_request(msg: dict) -> tuple[str, dict]`: Extract tool request details
92
+ - `wrap_tool_result(name: str, result: dict) -> dict`: Wrap tool result for agent
93
+ - `is_final_response(msg: dict) -> bool`: Check if message is a final response
94
+ - `get_final_response(msg: dict) -> dict`: Extract final response
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ # Install development dependencies
100
+ pip install -e .[dev]
101
+
102
+ # Run tests
103
+ pytest
104
+
105
+ # Format code
106
+ black .
107
+
108
+ # Lint code
109
+ flake8
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT License
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dacp/__init__.py
5
+ dacp/exceptions.py
6
+ dacp/llm.py
7
+ dacp/protocol.py
8
+ dacp/tools.py
9
+ dacp/types.py
10
+ dacp.egg-info/PKG-INFO
11
+ dacp.egg-info/SOURCES.txt
12
+ dacp.egg-info/dependency_links.txt
13
+ dacp.egg-info/requires.txt
14
+ dacp.egg-info/top_level.txt
15
+ tests/test_llm.py
16
+ tests/test_protocol.py
17
+ tests/test_tools.py
@@ -0,0 +1,9 @@
1
+ openai>=1.0.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0
5
+ pytest-cov>=4.0.0
6
+ black>=22.0.0
7
+ flake8>=5.0.0
8
+ mypy>=1.0.0
9
+ types-requests>=2.28.0
@@ -0,0 +1 @@
1
+ dacp
@@ -0,0 +1,101 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dacp"
7
+ version = "0.1.0"
8
+ description = "DACP - Declarative Agent Communication Protocol for LLM/agent communications and tool function calls"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Your Name", email = "your.email@example.com"}
14
+ ]
15
+ keywords = ["llm", "agent", "tools", "openai", "communication"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ ]
27
+
28
+ dependencies = [
29
+ "openai>=1.0.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0.0",
35
+ "pytest-cov>=4.0.0",
36
+ "black>=22.0.0",
37
+ "flake8>=5.0.0",
38
+ "mypy>=1.0.0",
39
+ "types-requests>=2.28.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/yourusername/dacp"
44
+ Repository = "https://github.com/yourusername/dacp"
45
+ Issues = "https://github.com/yourusername/dacp/issues"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["."]
49
+ include = ["dacp*"]
50
+ exclude = ["tests*"]
51
+
52
+ [tool.black]
53
+ line-length = 88
54
+ target-version = ['py38']
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ python_files = ["test_*.py"]
59
+ addopts = "--cov=dacp --cov-report=term-missing --cov-report=html"
60
+
61
+ [tool.coverage.run]
62
+ source = ["dacp"]
63
+ omit = ["tests/*"]
64
+
65
+ [tool.coverage.report]
66
+ exclude_lines = [
67
+ "pragma: no cover",
68
+ "def __repr__",
69
+ "if self.debug:",
70
+ "if settings.DEBUG",
71
+ "raise AssertionError",
72
+ "raise NotImplementedError",
73
+ "if 0:",
74
+ "if __name__ == .__main__.:",
75
+ "class .*\\bProtocol\\):",
76
+ "@(abc\\.)?abstractmethod",
77
+ ]
78
+
79
+ [tool.flake8]
80
+ max-line-length = 88
81
+
82
+ [tool.mypy]
83
+ python_version = "3.8"
84
+ warn_return_any = true
85
+ warn_unused_configs = true
86
+ disallow_untyped_defs = true
87
+ disallow_incomplete_defs = true
88
+ check_untyped_defs = true
89
+ disallow_untyped_decorators = true
90
+ no_implicit_optional = true
91
+ warn_redundant_casts = true
92
+ warn_unused_ignores = true
93
+ warn_no_return = true
94
+ warn_unreachable = true
95
+ strict_equality = true
96
+
97
+ [[tool.mypy.overrides]]
98
+ module = [
99
+ "openai.*",
100
+ ]
101
+ ignore_missing_imports = true
dacp-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,116 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ import os
4
+ from dacp.llm import call_llm
5
+
6
+
7
+ @patch("dacp.llm.openai")
8
+ def test_call_llm_success(mock_openai):
9
+ """Test successful LLM call."""
10
+ # Mock the OpenAI client and response
11
+ mock_client = MagicMock()
12
+ mock_openai.OpenAI.return_value = mock_client
13
+
14
+ mock_response = MagicMock()
15
+ mock_response.choices = [MagicMock()]
16
+ mock_response.choices[0].message.content = "Hello, world!"
17
+ mock_client.chat.completions.create.return_value = mock_response
18
+
19
+ # Test the function
20
+ result = call_llm("Test prompt")
21
+
22
+ # Verify the result
23
+ assert result == "Hello, world!"
24
+
25
+ # Verify OpenAI was called correctly
26
+ mock_openai.OpenAI.assert_called_once_with(
27
+ api_key=os.getenv("OPENAI_API_KEY"), base_url="https://api.openai.com/v1"
28
+ )
29
+ mock_client.chat.completions.create.assert_called_once_with(
30
+ model="gpt-4",
31
+ messages=[{"role": "user", "content": "Test prompt"}],
32
+ temperature=0.7,
33
+ max_tokens=150,
34
+ )
35
+
36
+
37
+ @patch("dacp.llm.openai")
38
+ def test_call_llm_custom_model(mock_openai):
39
+ """Test LLM call with custom model."""
40
+ mock_client = MagicMock()
41
+ mock_openai.OpenAI.return_value = mock_client
42
+
43
+ mock_response = MagicMock()
44
+ mock_response.choices = [MagicMock()]
45
+ mock_response.choices[0].message.content = "Custom model response"
46
+ mock_client.chat.completions.create.return_value = mock_response
47
+
48
+ # Test with custom model
49
+ result = call_llm("Test prompt", model="gpt-3.5-turbo")
50
+
51
+ assert result == "Custom model response"
52
+
53
+ # Verify custom model was used
54
+ mock_client.chat.completions.create.assert_called_once_with(
55
+ model="gpt-3.5-turbo",
56
+ messages=[{"role": "user", "content": "Test prompt"}],
57
+ temperature=0.7,
58
+ max_tokens=150,
59
+ )
60
+
61
+
62
+ @patch("dacp.llm.openai")
63
+ def test_call_llm_api_error(mock_openai):
64
+ """Test LLM call with API error."""
65
+ mock_client = MagicMock()
66
+ mock_openai.OpenAI.return_value = mock_client
67
+
68
+ # Mock an API error
69
+ mock_client.chat.completions.create.side_effect = Exception("API Error")
70
+
71
+ # Test that the error is raised
72
+ with pytest.raises(Exception):
73
+ call_llm("Test prompt")
74
+
75
+
76
+ @patch("dacp.llm.openai")
77
+ def test_call_llm_environment_variable(mock_openai):
78
+ """Test that the function uses the OPENAI_API_KEY environment variable."""
79
+ # Set a test API key
80
+ test_api_key = "test-api-key-123"
81
+ with patch.dict(os.environ, {"OPENAI_API_KEY": test_api_key}):
82
+ mock_client = MagicMock()
83
+ mock_openai.OpenAI.return_value = mock_client
84
+
85
+ mock_response = MagicMock()
86
+ mock_response.choices = [MagicMock()]
87
+ mock_response.choices[0].message.content = "Test response"
88
+ mock_client.chat.completions.create.return_value = mock_response
89
+
90
+ call_llm("Test prompt")
91
+
92
+ # Verify the API key was used
93
+ mock_openai.OpenAI.assert_called_once_with(
94
+ api_key=test_api_key, base_url="https://api.openai.com/v1"
95
+ )
96
+
97
+
98
+ @patch("dacp.llm.openai")
99
+ def test_call_llm_default_parameters(mock_openai):
100
+ """Test that default parameters are used correctly."""
101
+ mock_client = MagicMock()
102
+ mock_openai.OpenAI.return_value = mock_client
103
+
104
+ mock_response = MagicMock()
105
+ mock_response.choices = [MagicMock()]
106
+ mock_response.choices[0].message.content = "Default response"
107
+ mock_client.chat.completions.create.return_value = mock_response
108
+
109
+ call_llm("Test prompt")
110
+
111
+ # Verify default parameters
112
+ call_args = mock_client.chat.completions.create.call_args
113
+ assert call_args[1]["model"] == "gpt-4"
114
+ assert call_args[1]["temperature"] == 0.7
115
+ assert call_args[1]["max_tokens"] == 150
116
+ assert call_args[1]["messages"] == [{"role": "user", "content": "Test prompt"}]
@@ -0,0 +1,116 @@
1
+ import pytest
2
+ import json
3
+ from dacp.protocol import (
4
+ parse_agent_response,
5
+ is_tool_request,
6
+ get_tool_request,
7
+ wrap_tool_result,
8
+ is_final_response,
9
+ get_final_response,
10
+ )
11
+
12
+
13
+ def test_parse_agent_response_dict():
14
+ """Test parsing agent response that's already a dict."""
15
+ response = {"tool_request": {"name": "test_tool", "args": {"param": "value"}}}
16
+ result = parse_agent_response(response)
17
+ assert result == response
18
+
19
+
20
+ def test_parse_agent_response_string():
21
+ """Test parsing agent response that's a JSON string."""
22
+ response_dict = {"tool_request": {"name": "test_tool", "args": {"param": "value"}}}
23
+ response_string = json.dumps(response_dict)
24
+ result = parse_agent_response(response_string)
25
+ assert result == response_dict
26
+
27
+
28
+ def test_parse_agent_response_invalid_json():
29
+ """Test parsing invalid JSON string."""
30
+ with pytest.raises(ValueError, match="Malformed agent response"):
31
+ parse_agent_response("invalid json")
32
+
33
+
34
+ def test_is_tool_request_true():
35
+ """Test is_tool_request returns True for tool requests."""
36
+ msg = {"tool_request": {"name": "test_tool", "args": {}}}
37
+ assert is_tool_request(msg) is True
38
+
39
+
40
+ def test_is_tool_request_false():
41
+ """Test is_tool_request returns False for non-tool requests."""
42
+ msg = {"final_response": {"content": "Hello"}}
43
+ assert is_tool_request(msg) is False
44
+
45
+
46
+ def test_get_tool_request():
47
+ """Test getting tool request details."""
48
+ msg = {"tool_request": {"name": "test_tool", "args": {"param": "value"}}}
49
+ name, args = get_tool_request(msg)
50
+ assert name == "test_tool"
51
+ assert args == {"param": "value"}
52
+
53
+
54
+ def test_get_tool_request_no_args():
55
+ """Test getting tool request with no args."""
56
+ msg = {"tool_request": {"name": "test_tool"}}
57
+ name, args = get_tool_request(msg)
58
+ assert name == "test_tool"
59
+ assert args == {}
60
+
61
+
62
+ def test_wrap_tool_result():
63
+ """Test wrapping tool result."""
64
+ result = {"output": "success"}
65
+ wrapped = wrap_tool_result("test_tool", result)
66
+ expected = {"tool_result": {"name": "test_tool", "result": result}}
67
+ assert wrapped == expected
68
+
69
+
70
+ def test_is_final_response_true():
71
+ """Test is_final_response returns True for final responses."""
72
+ msg = {"final_response": {"content": "Hello"}}
73
+ assert is_final_response(msg) is True
74
+
75
+
76
+ def test_is_final_response_false():
77
+ """Test is_final_response returns False for non-final responses."""
78
+ msg = {"tool_request": {"name": "test_tool", "args": {}}}
79
+ assert is_final_response(msg) is False
80
+
81
+
82
+ def test_get_final_response():
83
+ """Test getting final response content."""
84
+ final_content = {"content": "Hello, world!"}
85
+ msg = {"final_response": final_content}
86
+ result = get_final_response(msg)
87
+ assert result == final_content
88
+
89
+
90
+ def test_complex_agent_response():
91
+ """Test parsing a complex agent response."""
92
+ complex_response = {
93
+ "tool_request": {
94
+ "name": "weather_api",
95
+ "args": {"location": "New York", "units": "celsius"},
96
+ },
97
+ "metadata": {"timestamp": "2024-01-01T00:00:00Z", "session_id": "abc123"},
98
+ }
99
+
100
+ # Test parsing
101
+ parsed = parse_agent_response(complex_response)
102
+ assert parsed == complex_response
103
+
104
+ # Test tool request detection
105
+ assert is_tool_request(parsed) is True
106
+
107
+ # Test getting tool request
108
+ name, args = get_tool_request(parsed)
109
+ assert name == "weather_api"
110
+ assert args == {"location": "New York", "units": "celsius"}
111
+
112
+ # Test wrapping result
113
+ tool_result = {"temperature": 20, "condition": "sunny"}
114
+ wrapped = wrap_tool_result(name, tool_result)
115
+ assert wrapped["tool_result"]["name"] == "weather_api"
116
+ assert wrapped["tool_result"]["result"] == tool_result
@@ -0,0 +1,72 @@
1
+ import pytest
2
+ from dacp.tools import register_tool, run_tool, TOOL_REGISTRY
3
+
4
+
5
+ def test_register_tool():
6
+ """Test registering a tool."""
7
+
8
+ def test_tool(param1: str, param2: int) -> dict:
9
+ return {"result": f"Processed {param1} with {param2}"}
10
+
11
+ register_tool("test_tool", test_tool)
12
+ assert "test_tool" in TOOL_REGISTRY
13
+ assert TOOL_REGISTRY["test_tool"] == test_tool
14
+
15
+
16
+ def test_run_tool():
17
+ """Test running a registered tool."""
18
+
19
+ def test_tool(param1: str, param2: int) -> dict:
20
+ return {"result": f"Processed {param1} with {param2}"}
21
+
22
+ register_tool("test_tool", test_tool)
23
+ result = run_tool("test_tool", {"param1": "hello", "param2": 42})
24
+ assert result["result"] == "Processed hello with 42"
25
+
26
+
27
+ def test_run_nonexistent_tool():
28
+ """Test running a tool that doesn't exist."""
29
+ with pytest.raises(ValueError):
30
+ run_tool("nonexistent_tool", {})
31
+
32
+
33
+ def test_tool_with_no_args():
34
+ """Test running a tool with no arguments."""
35
+
36
+ def no_args_tool() -> dict:
37
+ return {"result": "success"}
38
+
39
+ register_tool("no_args_tool", no_args_tool)
40
+ result = run_tool("no_args_tool", {})
41
+ assert result["result"] == "success"
42
+
43
+
44
+ def test_tool_with_optional_args():
45
+ """Test running a tool with optional arguments."""
46
+
47
+ def optional_args_tool(required: str, optional: str = "default") -> dict:
48
+ return {"result": f"{required}:{optional}"}
49
+
50
+ register_tool("optional_args_tool", optional_args_tool)
51
+
52
+ # Test with both args
53
+ result = run_tool("optional_args_tool", {"required": "test", "optional": "custom"})
54
+ assert result["result"] == "test:custom"
55
+
56
+ # Test with only required arg
57
+ result = run_tool("optional_args_tool", {"required": "test"})
58
+ assert result["result"] == "test:default"
59
+
60
+
61
+ def test_clear_tool_registry():
62
+ """Test that we can clear and re-register tools."""
63
+
64
+ def test_tool() -> dict:
65
+ return {"result": "test"}
66
+
67
+ register_tool("test_tool", test_tool)
68
+ assert "test_tool" in TOOL_REGISTRY
69
+
70
+ # Clear registry
71
+ TOOL_REGISTRY.clear()
72
+ assert "test_tool" not in TOOL_REGISTRY