Skip to content

Commit 3bb062e

Browse files
Add support for tool use and language model config (#123)
1 parent d3af4bc commit 3bb062e

17 files changed

+442
-18
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"pylint.args": ["--rcfile", "pyproject.toml"],
1818
"python.analysis.extraPaths": ["tests"],
1919
"python.testing.cwd": "tests",
20-
"cSpell.words": ["agen", "cffi", "deepgram", "indata", "samplerate", "simpleaudio", "sounddevice"]
20+
"cSpell.words": ["agen", "cffi", "deepgram", "indata", "metas", "samplerate", "simpleaudio", "sounddevice"]
2121
}

hume/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@
1111
TranscriptionConfig,
1212
)
1313
from hume._measurement.stream import HumeStreamClient, StreamSocket
14-
from hume._voice import HumeVoiceClient, MicrophoneInterface, VoiceChat, VoiceConfig, VoiceSocket
14+
from hume._voice import (
15+
HumeVoiceClient,
16+
LanguageModelConfig,
17+
MicrophoneInterface,
18+
VoiceChat,
19+
VoiceConfig,
20+
VoiceIdentityConfig,
21+
VoiceSocket,
22+
VoiceTool,
23+
)
1524
from hume.error.hume_client_exception import HumeClientException
1625

1726
__version__ = version("hume")
@@ -26,10 +35,13 @@
2635
"HumeClientException",
2736
"HumeStreamClient",
2837
"HumeVoiceClient",
38+
"LanguageModelConfig",
2939
"MicrophoneInterface",
3040
"StreamSocket",
3141
"TranscriptionConfig",
3242
"VoiceChat",
3343
"VoiceConfig",
44+
"VoiceIdentityConfig",
3445
"VoiceSocket",
46+
"VoiceTool",
3547
]

hume/_common/client_base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def _request(
115115
response = self._http_client.send(request)
116116
response.raise_for_status()
117117
except Exception as exc: # pylint: disable=broad-exception-caught
118-
raise HumeClientException(str(exc)) from exc
118+
response_body = response.json()
119+
raise HumeClientException(str(response_body)) from exc
119120

120121
return response
121122

hume/_voice/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
from hume._voice.hume_voice_client import HumeVoiceClient
44
from hume._voice.microphone.microphone_interface import MicrophoneInterface
55
from hume._voice.models.chats_models import VoiceChat
6-
from hume._voice.models.configs_models import VoiceConfig
6+
from hume._voice.models.configs_models import LanguageModelConfig, VoiceConfig, VoiceIdentityConfig
7+
from hume._voice.models.tools_models import VoiceTool
78
from hume._voice.voice_socket import VoiceSocket
89

910
__all__ = [
1011
"HumeVoiceClient",
12+
"LanguageModelConfig",
1113
"MicrophoneInterface",
1214
"VoiceChat",
1315
"VoiceConfig",
16+
"VoiceIdentityConfig",
1417
"VoiceSocket",
18+
"VoiceTool",
1519
]

hume/_voice/hume_voice_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from hume._voice.mixins.chat_mixin import ChatMixin
66
from hume._voice.mixins.chats_mixin import ChatsMixin
77
from hume._voice.mixins.configs_mixin import ConfigsMixin
8+
from hume._voice.mixins.tools_mixin import ToolsMixin
89

910
logger = logging.getLogger(__name__)
1011

1112

12-
class HumeVoiceClient(ChatMixin, ChatsMixin, ConfigsMixin):
13+
class HumeVoiceClient(ChatMixin, ChatsMixin, ConfigsMixin, ToolsMixin):
1314
"""Empathic Voice Interface client."""

hume/_voice/microphone/chat_client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def _map_role(cls, role: str) -> str:
4444
return cls.DEFAULT_ASSISTANT_ROLE_NAME
4545
return role
4646

47+
def _print_prompt(self, text: str) -> None:
48+
now = datetime.datetime.now(tz=datetime.timezone.utc)
49+
now_str = now.strftime("%H:%M:%S")
50+
print(f"[{now_str}] {text}")
51+
4752
async def _recv(self, *, socket: VoiceSocket) -> None:
4853
async for socket_message in socket:
4954
message = json.loads(socket_message)
@@ -60,14 +65,22 @@ async def _recv(self, *, socket: VoiceSocket) -> None:
6065
error_message: str = message["message"]
6166
error_code: str = message["code"]
6267
raise HumeClientException(f"Error ({error_code}): {error_message}")
68+
elif message["type"] == "tool_call":
69+
print(
70+
"Warning: EVI is trying to make a tool call. "
71+
"Either remove tool calling from your config or "
72+
"use the VoiceSocket directly without a MicrophoneInterface."
73+
)
74+
tool_call_id = message["tool_call_id"]
75+
if message["response_required"]:
76+
content = "Let's start over"
77+
await self.sender.send_tool_response(socket=socket, tool_call_id=tool_call_id, content=content)
78+
continue
6379
else:
6480
message_type = message["type"].upper()
6581
text = f"<{message_type}>"
6682

67-
now = datetime.datetime.now(tz=datetime.timezone.utc)
68-
now_str = now.strftime("%H:%M:%S")
69-
70-
print(f"[{now_str}] {text}")
83+
self._print_prompt(text)
7184

7285
async def _play(self) -> None:
7386
async for byte_str in self.byte_strs:

hume/_voice/microphone/microphone_sender.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Sender for streaming audio from a microphone."""
22

3+
import json
34
import logging
45
from dataclasses import dataclass
56
from typing import Protocol
@@ -29,6 +30,16 @@ async def send(self, *, socket: VoiceSocket) -> None:
2930
"""
3031
raise NotImplementedError()
3132

33+
async def send_tool_response(self, *, socket: VoiceSocket, tool_call_id: str, content: str) -> None:
34+
"""Send a tool response over an EVI socket.
35+
36+
Args:
37+
socket (VoiceSocket): EVI socket.
38+
tool_call_id (str): Tool call ID.
39+
content (str): Tool response content.
40+
"""
41+
raise NotImplementedError()
42+
3243

3344
@dataclass
3445
class MicrophoneSender(Sender):
@@ -65,3 +76,18 @@ async def send(self, *, socket: VoiceSocket) -> None:
6576
async for byte_str in self.microphone:
6677
if self.send_audio:
6778
await socket.send(byte_str)
79+
80+
async def send_tool_response(self, *, socket: VoiceSocket, tool_call_id: str, content: str) -> None:
81+
"""Send a tool response over an EVI socket.
82+
83+
Args:
84+
socket (VoiceSocket): EVI socket.
85+
tool_call_id (str): Tool call ID.
86+
content (str): Tool response content.
87+
"""
88+
response_message = {
89+
"type": "tool_response",
90+
"tool_call_id": tool_call_id,
91+
"content": content,
92+
}
93+
await socket.send(json.dumps(response_message).encode("utf-8"))

hume/_voice/mixins/configs_mixin.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
"""Client operations for managing EVI configurations."""
22

33
import logging
4-
from typing import Iterator, Optional
4+
from typing import Iterator, List, Optional
55

66
from hume._common.client_base import ClientBase
77
from hume._common.utilities.paging_utilities import Paging
88
from hume._voice.models.configs_models import (
99
ConfigResponse,
1010
ConfigsResponse,
11+
LanguageModelConfig,
1112
PostConfigRequest,
1213
PostPromptRequest,
1314
PromptMeta,
1415
PromptResponse,
1516
VoiceConfig,
1617
VoiceIdentityConfig,
1718
)
19+
from hume._voice.models.tools_models import ToolMeta, VoiceTool
1820
from hume.error.hume_client_exception import HumeClientException
1921

2022
logger = logging.getLogger(__name__)
@@ -32,7 +34,9 @@ def create_config(
3234
name: str,
3335
prompt: str,
3436
description: Optional[str] = None,
35-
voice_name: Optional[str] = DEFAULT_VOICE_NAME,
37+
voice_identity_config: Optional[VoiceIdentityConfig] = None,
38+
tools: Optional[List[VoiceTool]] = None,
39+
language_model: Optional[LanguageModelConfig] = None,
3640
) -> VoiceConfig:
3741
"""Create a new EVI config.
3842
@@ -48,11 +52,14 @@ def create_config(
4852
prompt_response = PromptResponse.model_validate_json(response.text)
4953
prompt_meta = PromptMeta(id=prompt_response.id, version=prompt_response.version)
5054

55+
tool_metas = None if tools is None else [ToolMeta(id=tool.id, version=0) for tool in tools]
5156
post_config_request = PostConfigRequest(
5257
name=name,
5358
version_description=description,
5459
prompt=prompt_meta,
55-
voice=VoiceIdentityConfig(name=voice_name),
60+
voice=voice_identity_config,
61+
tools=tool_metas,
62+
language_model=language_model,
5663
)
5764
post_config_body = post_config_request.to_json_str()
5865
endpoint = self._build_endpoint("evi", "configs")

hume/_voice/mixins/tools_mixin.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Client operations for managing EVI tools."""
2+
3+
import logging
4+
from typing import Iterator, Optional
5+
6+
from hume._common.client_base import ClientBase
7+
from hume._common.utilities.paging_utilities import Paging
8+
from hume._voice.models.tools_models import PostToolRequest, ToolResponse, ToolsResponse, VoiceTool
9+
from hume.error.hume_client_exception import HumeClientException
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
# pylint: disable=redefined-builtin
15+
class ToolsMixin(ClientBase):
16+
"""Client operations for managing EVI tools."""
17+
18+
def create_tool(
19+
self,
20+
*,
21+
name: str,
22+
parameters: str,
23+
fallback_content: Optional[str] = None,
24+
description: Optional[str] = None,
25+
) -> VoiceTool:
26+
"""Create a new EVI tool.
27+
28+
Args:
29+
name (str): Tool name.
30+
parameters (str): Stringified JSON defining the parameters used by the tool.
31+
fallback_content (Optional[str]): Text to use if the tool fails to generate content.
32+
description (Optional[str]): Tool description.
33+
"""
34+
post_tool_request = PostToolRequest(
35+
name=name,
36+
description=description,
37+
version_description=None,
38+
parameters=parameters,
39+
fallback_content=fallback_content,
40+
)
41+
post_tool_body = post_tool_request.to_json_str()
42+
endpoint = self._build_endpoint("evi", "tools")
43+
response = self._request(endpoint, method="POST", body_json_str=post_tool_body)
44+
tool_response = ToolResponse.model_validate_json(response.text)
45+
46+
return self._tool_from_response(tool_response)
47+
48+
def get_tool(self, id: str, _version: Optional[int] = None) -> VoiceTool:
49+
"""Get an EVI tool by its ID.
50+
51+
Args:
52+
id (str): Tool ID.
53+
"""
54+
route = f"tools/{id}" if _version is None else f"tools/{id}/version/{_version}"
55+
endpoint = self._build_endpoint("evi", route)
56+
response = self._request(endpoint, method="GET")
57+
tools_response = ToolsResponse.model_validate_json(response.text)
58+
if len(tools_response.tools_page) == 0:
59+
raise HumeClientException(f"Tool not found with ID: {id}")
60+
61+
return self._tool_from_response(tools_response.tools_page[0])
62+
63+
def _iter_tool_versions(self, id: str) -> Iterator[VoiceTool]:
64+
endpoint = self._build_endpoint("evi", f"tools/{id}")
65+
for page_number in range(self.PAGING_LIMIT):
66+
paging = Paging(page_size=self._page_size, page_number=page_number)
67+
response = self._request(endpoint, method="GET", paging=paging)
68+
tools_response = ToolsResponse.model_validate_json(response.text)
69+
if len(tools_response.tools_page) == 0:
70+
break
71+
for res in tools_response.tools_page:
72+
yield self._tool_from_response(res)
73+
74+
def _tool_from_response(self, tool_response: ToolResponse) -> VoiceTool:
75+
return VoiceTool(
76+
id=tool_response.id,
77+
name=tool_response.name,
78+
created_on=tool_response.created_on,
79+
modified_on=tool_response.modified_on,
80+
parameters=tool_response.parameters,
81+
description=tool_response.description,
82+
fallback_content=tool_response.fallback_content,
83+
)
84+
85+
def iter_tools(self) -> Iterator[VoiceTool]:
86+
"""Iterate over existing EVI tools."""
87+
endpoint = self._build_endpoint("evi", "tools")
88+
for page_number in range(self.PAGING_LIMIT):
89+
paging = Paging(page_size=self._page_size, page_number=page_number)
90+
response = self._request(endpoint, method="GET", paging=paging)
91+
tools_response = ToolsResponse.model_validate_json(response.text)
92+
if len(tools_response.tools_page) == 0:
93+
break
94+
for res in tools_response.tools_page:
95+
yield self._tool_from_response(res)
96+
97+
def delete_tool(self, id: str, _version: Optional[int] = None) -> None:
98+
"""Delete an EVI tool.
99+
100+
Args:
101+
id (str): Tool ID.
102+
"""
103+
route = f"tools/{id}" if _version is None else f"tools/{id}/version/{_version}"
104+
endpoint = self._build_endpoint("evi", route)
105+
self._request(endpoint, method="DELETE")

hume/_voice/models/configs_models.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List, Optional
44

55
from hume._common.utilities.model_utilities import BaseModel
6+
from hume._voice.models.tools_models import ToolMeta
67

78

89
class PromptResponse(BaseModel):
@@ -32,6 +33,14 @@ class PromptMeta(BaseModel):
3233
version: int
3334

3435

36+
class LanguageModelConfig(BaseModel):
37+
"""Language model configuration for EVI."""
38+
39+
model_provider: str
40+
model_resource: str
41+
temperature: Optional[float] = None
42+
43+
3544
class PostPromptRequest(BaseModel):
3645
"""Post request model for creating a new EVI prompt."""
3746

@@ -67,13 +76,23 @@ class VoiceIdentityConfig(BaseModel):
6776
name: Optional[str] = None
6877

6978

79+
class BuiltinToolConfig(BaseModel):
80+
"""Configuration for a built-in EVI tool."""
81+
82+
name: str
83+
tool_type: str
84+
fallback_content: Optional[str]
85+
86+
7087
class PostConfigRequest(BaseModel):
7188
"""Post request model for creating a new EVI configuration."""
7289

7390
name: str
7491
version_description: Optional[str]
7592
prompt: PromptMeta
76-
voice: VoiceIdentityConfig
93+
voice: Optional[VoiceIdentityConfig]
94+
language_model: Optional[LanguageModelConfig]
95+
tools: Optional[List[ToolMeta]]
7796

7897

7998
class ConfigMeta(BaseModel):
@@ -91,4 +110,5 @@ class VoiceConfig(BaseModel):
91110
description: Optional[str]
92111
created_on: int
93112
modified_on: int
113+
# TODO: Add tool info
94114
prompt: Optional[str]

0 commit comments

Comments
 (0)