parent
e7ccbe0efa
commit
d37303de6a
10 changed files with 157 additions and 167 deletions
|
|
@ -12,8 +12,6 @@ from arcade.actor.core.components import (
|
||||||
from arcade.core.catalog import ToolCatalog, Toolkit
|
from arcade.core.catalog import ToolCatalog, Toolkit
|
||||||
from arcade.core.executor import ToolExecutor
|
from arcade.core.executor import ToolExecutor
|
||||||
from arcade.core.schema import (
|
from arcade.core.schema import (
|
||||||
ToolCallError,
|
|
||||||
ToolCallOutput,
|
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
ToolCallResponse,
|
ToolCallResponse,
|
||||||
ToolDefinition,
|
ToolDefinition,
|
||||||
|
|
@ -71,7 +69,7 @@ class BaseActor(Actor):
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
response = await ToolExecutor.run(
|
output = await ToolExecutor.run(
|
||||||
func=materialized_tool.tool,
|
func=materialized_tool.tool,
|
||||||
definition=materialized_tool.definition,
|
definition=materialized_tool.definition,
|
||||||
input_model=materialized_tool.input_model,
|
input_model=materialized_tool.input_model,
|
||||||
|
|
@ -79,17 +77,6 @@ class BaseActor(Actor):
|
||||||
context=tool_request.context,
|
context=tool_request.context,
|
||||||
**tool_request.inputs or {},
|
**tool_request.inputs or {},
|
||||||
)
|
)
|
||||||
if response.code == 200 and response.data is not None:
|
|
||||||
output = (
|
|
||||||
ToolCallOutput(value=response.data.result)
|
|
||||||
if hasattr(response.data, "result") and response.data.result
|
|
||||||
else ToolCallOutput(value=f"Tool {tool_name} called successfully")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# TODO flatten this to just ToolCallError
|
|
||||||
output = ToolCallOutput(error=ToolCallError(message=response.msg))
|
|
||||||
if response.code == 425:
|
|
||||||
output.error.additional_prompt_content = response.additional_prompt_content
|
|
||||||
|
|
||||||
end_time = time.time() # End time in seconds
|
end_time = time.time() # End time in seconds
|
||||||
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||||
|
|
@ -98,7 +85,7 @@ class BaseActor(Actor):
|
||||||
invocation_id=tool_request.invocation_id,
|
invocation_id=tool_request.invocation_id,
|
||||||
duration=duration_ms,
|
duration=duration_ms,
|
||||||
finished_at=datetime.now().isoformat(),
|
finished_at=datetime.now().isoformat(),
|
||||||
success=response.code == 200,
|
success=not output.error,
|
||||||
output=output,
|
output=output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from typer.models import Context
|
||||||
from arcade.core.catalog import ToolCatalog
|
from arcade.core.catalog import ToolCatalog
|
||||||
from arcade.core.client import EngineClient
|
from arcade.core.client import EngineClient
|
||||||
from arcade.core.config import Config
|
from arcade.core.config import Config
|
||||||
from arcade.core.schema import ToolContext
|
from arcade.core.schema import ToolCallOutput, ToolContext
|
||||||
from arcade.core.toolkit import Toolkit
|
from arcade.core.toolkit import Toolkit
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ def run(
|
||||||
console.print(f"Calling tool: {tool_name} with params: {parameters}", style="bold blue")
|
console.print(f"Calling tool: {tool_name} with params: {parameters}", style="bold blue")
|
||||||
|
|
||||||
# TODO async.gather instead of loop.
|
# TODO async.gather instead of loop.
|
||||||
output = asyncio.run(
|
output: ToolCallOutput = asyncio.run(
|
||||||
ToolExecutor.run(
|
ToolExecutor.run(
|
||||||
called_tool.tool,
|
called_tool.tool,
|
||||||
called_tool.definition,
|
called_tool.definition,
|
||||||
|
|
@ -155,22 +155,20 @@ def run(
|
||||||
**parameters,
|
**parameters,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if output.code != 200:
|
if output.error:
|
||||||
console.print(output.msg, style="bold red")
|
console.print(output.error.message, style="bold red")
|
||||||
if output.data:
|
typer.Exit(code=1)
|
||||||
console.print(output.data.result, style="bold red")
|
|
||||||
typer.Exit(code=1)
|
|
||||||
else:
|
else:
|
||||||
messages += [
|
messages += [
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
# TODO: escape the output and ensure serialization works
|
# TODO: escape the output and ensure serialization works
|
||||||
"content": f"Results of Tool {tool_name}: {output.data.result!s}", # type: ignore[union-attr]
|
"content": f"Results of Tool {tool_name}: {output.value!s}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if choice == "execute":
|
if choice == "execute":
|
||||||
console.print(output.data.result, style="green") # type: ignore[union-attr]
|
console.print(output.value, style="green")
|
||||||
raise typer.Exit(0)
|
raise typer.Exit(0)
|
||||||
else:
|
else:
|
||||||
if stream:
|
if stream:
|
||||||
|
|
@ -206,9 +204,18 @@ def chat(
|
||||||
|
|
||||||
client = EngineClient(base_url=config.engine_url)
|
client = EngineClient(base_url=config.engine_url)
|
||||||
|
|
||||||
|
if config.user and config.user.email:
|
||||||
|
user_email = config.user.email
|
||||||
|
user_attribution = f"({user_email})"
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
"❌ User email not found in configuration. Please run `arcade login`.", style="bold red"
|
||||||
|
)
|
||||||
|
typer.Exit(code=1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# start messages conversation
|
# start messages conversation
|
||||||
messages = []
|
messages: list[dict[str, Any]] = []
|
||||||
|
|
||||||
chat_header = Text.assemble(
|
chat_header = Text.assemble(
|
||||||
"\n",
|
"\n",
|
||||||
|
|
@ -220,12 +227,9 @@ def chat(
|
||||||
)
|
)
|
||||||
console.print(chat_header)
|
console.print(chat_header)
|
||||||
|
|
||||||
user = config.user.email if config.user and config.user.email else None
|
|
||||||
user_attribution = f" ({user})" if user else ""
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
user_input = console.input(
|
user_input = console.input(
|
||||||
f"\n[magenta][bold]User[/bold]{user_attribution}:[/magenta] "
|
f"\n[magenta][bold]User[/bold] {user_attribution}:[/magenta] "
|
||||||
)
|
)
|
||||||
messages.append({"role": "user", "content": user_input})
|
messages.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
|
@ -234,7 +238,7 @@ def chat(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
tool_choice="generate",
|
tool_choice="generate",
|
||||||
user=user,
|
user=user_email,
|
||||||
)
|
)
|
||||||
role, message = display_streamed_markdown(stream_response)
|
role, message = display_streamed_markdown(stream_response)
|
||||||
messages.append({"role": role, "content": message})
|
messages.append({"role": role, "content": message})
|
||||||
|
|
@ -243,7 +247,7 @@ def chat(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
tool_choice="generate",
|
tool_choice="generate",
|
||||||
user=user,
|
user=user_email,
|
||||||
)
|
)
|
||||||
message_content = response.choices[0].message.content or ""
|
message_content = response.choices[0].message.content or ""
|
||||||
role = response.choices[0].message.role
|
role = response.choices[0].message.role
|
||||||
|
|
@ -380,7 +384,7 @@ def display_config_as_table(config: Config) -> None:
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
def display_streamed_markdown(stream: Stream[ChatCompletionChunk]) -> tuple[str, dict[str, Any]]:
|
def display_streamed_markdown(stream: Stream[ChatCompletionChunk]) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Display the streamed markdown chunks as a single line.
|
Display the streamed markdown chunks as a single line.
|
||||||
"""
|
"""
|
||||||
|
|
@ -393,7 +397,7 @@ def display_streamed_markdown(stream: Stream[ChatCompletionChunk]) -> tuple[str,
|
||||||
choice = chunk.choices[0]
|
choice = chunk.choices[0]
|
||||||
chunk_message = choice.delta.content
|
chunk_message = choice.delta.content
|
||||||
if role == "":
|
if role == "":
|
||||||
role = choice.delta.role
|
role = choice.delta.role or ""
|
||||||
if role == "assistant":
|
if role == "assistant":
|
||||||
console.print("\n[bold blue]Assistant:[/bold blue] ")
|
console.print("\n[bold blue]Assistant:[/bold blue] ")
|
||||||
if chunk_message:
|
if chunk_message:
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ class ToolDefinitionError(ToolError):
|
||||||
|
|
||||||
|
|
||||||
class ToolRuntimeError(RuntimeError):
|
class ToolRuntimeError(RuntimeError):
|
||||||
def __init__(self, message: str):
|
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.developer_message = developer_message
|
||||||
|
|
||||||
|
|
||||||
class ToolExecutionError(ToolRuntimeError):
|
class ToolExecutionError(ToolRuntimeError):
|
||||||
|
|
@ -32,8 +33,7 @@ class ToolExecutionError(ToolRuntimeError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message: str, developer_message: Optional[str] = None):
|
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||||
super().__init__(message)
|
super().__init__(message, developer_message)
|
||||||
self.developer_message = developer_message
|
|
||||||
|
|
||||||
|
|
||||||
class RetryableToolError(ToolExecutionError):
|
class RetryableToolError(ToolExecutionError):
|
||||||
|
|
@ -46,10 +46,11 @@ class RetryableToolError(ToolExecutionError):
|
||||||
message: str,
|
message: str,
|
||||||
developer_message: Optional[str] = None,
|
developer_message: Optional[str] = None,
|
||||||
additional_prompt_content: Optional[str] = None,
|
additional_prompt_content: Optional[str] = None,
|
||||||
|
retry_after_ms: Optional[int] = None,
|
||||||
):
|
):
|
||||||
super().__init__(message)
|
super().__init__(message, developer_message)
|
||||||
self.developer_message = developer_message
|
|
||||||
self.additional_prompt_content = additional_prompt_content
|
self.additional_prompt_content = additional_prompt_content
|
||||||
|
self.retry_after_ms = retry_after_ms
|
||||||
|
|
||||||
|
|
||||||
class ToolSerializationError(ToolRuntimeError):
|
class ToolSerializationError(ToolRuntimeError):
|
||||||
|
|
@ -57,7 +58,8 @@ class ToolSerializationError(ToolRuntimeError):
|
||||||
Raised when there is an error executing a tool.
|
Raised when there is an error executing a tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||||
|
super().__init__(message, developer_message)
|
||||||
|
|
||||||
|
|
||||||
class ToolInputError(ToolSerializationError):
|
class ToolInputError(ToolSerializationError):
|
||||||
|
|
@ -65,7 +67,8 @@ class ToolInputError(ToolSerializationError):
|
||||||
Raised when there is an error in the input to a tool.
|
Raised when there is an error in the input to a tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||||
|
super().__init__(message, developer_message)
|
||||||
|
|
||||||
|
|
||||||
class ToolOutputError(ToolSerializationError):
|
class ToolOutputError(ToolSerializationError):
|
||||||
|
|
@ -73,4 +76,5 @@ class ToolOutputError(ToolSerializationError):
|
||||||
Raised when there is an error in the output of a tool.
|
Raised when there is an error in the output of a tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def __init__(self, message: str, developer_message: Optional[str] = None):
|
||||||
|
super().__init__(message, developer_message)
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,12 @@ from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
from arcade.core.errors import (
|
from arcade.core.errors import (
|
||||||
RetryableToolError,
|
RetryableToolError,
|
||||||
ToolExecutionError,
|
|
||||||
ToolInputError,
|
ToolInputError,
|
||||||
ToolOutputError,
|
ToolOutputError,
|
||||||
ToolSerializationError,
|
ToolRuntimeError,
|
||||||
)
|
)
|
||||||
from arcade.core.response import ToolResponse, tool_response
|
from arcade.core.output import output_factory
|
||||||
from arcade.core.schema import ToolContext, ToolDefinition
|
from arcade.core.schema import ToolCallOutput, ToolContext, ToolDefinition
|
||||||
|
|
||||||
|
|
||||||
class ToolExecutor:
|
class ToolExecutor:
|
||||||
|
|
@ -24,7 +23,7 @@ class ToolExecutor:
|
||||||
context: ToolContext,
|
context: ToolContext,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> ToolResponse:
|
) -> ToolCallOutput:
|
||||||
"""
|
"""
|
||||||
Execute a callable function with validated inputs and outputs via Pydantic models.
|
Execute a callable function with validated inputs and outputs via Pydantic models.
|
||||||
"""
|
"""
|
||||||
|
|
@ -49,23 +48,30 @@ class ToolExecutor:
|
||||||
output = await ToolExecutor._serialize_output(output_model, results)
|
output = await ToolExecutor._serialize_output(output_model, results)
|
||||||
|
|
||||||
# return the output
|
# return the output
|
||||||
return tool_response.success(data=output)
|
return output_factory.success(data=output)
|
||||||
|
|
||||||
except RetryableToolError as e:
|
except RetryableToolError as e:
|
||||||
return tool_response.fail_retry(
|
return output_factory.fail_retry(
|
||||||
msg=str(e), additional_prompt_content=e.additional_prompt_content
|
message=e.message,
|
||||||
|
developer_message=e.developer_message,
|
||||||
|
additional_prompt_content=e.additional_prompt_content,
|
||||||
|
retry_after_ms=e.retry_after_ms,
|
||||||
)
|
)
|
||||||
|
|
||||||
except ToolSerializationError as e:
|
except ToolInputError as e:
|
||||||
return tool_response.fail(msg=str(e))
|
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
||||||
|
|
||||||
except ToolExecutionError as e:
|
except ToolOutputError as e:
|
||||||
return tool_response.fail(msg=str(e))
|
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
||||||
|
|
||||||
|
except ToolRuntimeError as e: # Catch any remaining tool-related errors
|
||||||
|
return output_factory.fail(
|
||||||
|
message=f"Error in execution: {e.message}", developer_message=e.developer_message
|
||||||
|
)
|
||||||
|
|
||||||
# if we get here we're in trouble
|
# if we get here we're in trouble
|
||||||
# TODO: Debate if this is necessary
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return tool_response.fail(msg=str(e))
|
return output_factory.fail(message="Error in execution", developer_message=str(e))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _serialize_input(input_model: type[BaseModel], **kwargs: Any) -> BaseModel:
|
async def _serialize_input(input_model: type[BaseModel], **kwargs: Any) -> BaseModel:
|
||||||
|
|
@ -79,7 +85,7 @@ class ToolExecutor:
|
||||||
inputs = input_model(**kwargs)
|
inputs = input_model(**kwargs)
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ToolInputError from e
|
raise ToolInputError(message="Error in input", developer_message=str(e)) from e
|
||||||
|
|
||||||
return inputs
|
return inputs
|
||||||
|
|
||||||
|
|
@ -97,6 +103,6 @@ class ToolExecutor:
|
||||||
output = output_model(**{"result": results})
|
output = output_model(**{"result": results})
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ToolOutputError from e
|
raise ToolOutputError(message="Error in output", developer_message=str(e)) from e
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
|
||||||
48
arcade/arcade/core/output.py
Normal file
48
arcade/arcade/core/output.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from arcade.core.schema import ToolCallError, ToolCallOutput
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class ToolOutputFactory:
|
||||||
|
"""
|
||||||
|
Singleton pattern for unified return method from tools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def success(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
data: T | None = None,
|
||||||
|
) -> ToolCallOutput:
|
||||||
|
value = data.result if data and hasattr(data, "result") and data.result else ""
|
||||||
|
|
||||||
|
return ToolCallOutput(value=value)
|
||||||
|
|
||||||
|
def fail(self, *, message: str, developer_message: str | None = None) -> ToolCallOutput:
|
||||||
|
return ToolCallOutput(
|
||||||
|
error=ToolCallError(
|
||||||
|
message=message, developer_message=developer_message, can_retry=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def fail_retry(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str,
|
||||||
|
developer_message: str | None = None,
|
||||||
|
additional_prompt_content: str | None = None,
|
||||||
|
retry_after_ms: int | None = None,
|
||||||
|
) -> ToolCallOutput:
|
||||||
|
return ToolCallOutput(
|
||||||
|
error=ToolCallError(
|
||||||
|
message=message,
|
||||||
|
developer_message=developer_message,
|
||||||
|
can_retry=True,
|
||||||
|
additional_prompt_content=additional_prompt_content,
|
||||||
|
retry_after_ms=retry_after_ms,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
output_factory = ToolOutputFactory()
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
from typing import Any, Generic, TypeVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from arcade.core.response_code import (
|
|
||||||
CustomResponse,
|
|
||||||
CustomResponseCode,
|
|
||||||
)
|
|
||||||
|
|
||||||
_ExcludeData = set[int | str] | dict[int | str, Any]
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Mapping of tool response actions to http codes?
|
|
||||||
|
|
||||||
|
|
||||||
class ToolResponse(BaseModel, Generic[T]):
|
|
||||||
"""
|
|
||||||
Generic unified return model for Tools
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
code: int = CustomResponseCode.HTTP_200.code
|
|
||||||
msg: str = CustomResponseCode.HTTP_200.msg
|
|
||||||
additional_prompt_content: str | None = None
|
|
||||||
|
|
||||||
#
|
|
||||||
data: T | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ToolResponseFactory:
|
|
||||||
"""
|
|
||||||
Singleton pattern for unified return method from tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __response(
|
|
||||||
*,
|
|
||||||
msg: str | None = None,
|
|
||||||
res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200,
|
|
||||||
data: T | None = None,
|
|
||||||
) -> ToolResponse:
|
|
||||||
"""
|
|
||||||
General method for successful response
|
|
||||||
"""
|
|
||||||
if msg:
|
|
||||||
return ToolResponse(code=res.code, msg=msg, data=data)
|
|
||||||
return ToolResponse(code=res.code, msg=res.msg, data=data)
|
|
||||||
|
|
||||||
def success(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200,
|
|
||||||
data: T | None = None,
|
|
||||||
) -> ToolResponse:
|
|
||||||
return self.__response(res=res, data=data)
|
|
||||||
|
|
||||||
def fail(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_400,
|
|
||||||
msg: str = CustomResponseCode.HTTP_400.msg,
|
|
||||||
data: Any = None,
|
|
||||||
) -> ToolResponse:
|
|
||||||
return self.__response(
|
|
||||||
res=res,
|
|
||||||
msg=msg, # TODO this needs to map to developer_message in output.error
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def fail_retry(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_425,
|
|
||||||
msg: str = CustomResponseCode.HTTP_425.msg,
|
|
||||||
data: Any = None,
|
|
||||||
additional_prompt_content: str | None = None,
|
|
||||||
) -> ToolResponse:
|
|
||||||
res = self.__response(
|
|
||||||
res=res,
|
|
||||||
msg=msg,
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
res.additional_prompt_content = additional_prompt_content
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
tool_response = ToolResponseFactory()
|
|
||||||
|
|
@ -168,7 +168,12 @@ class ToolCallError(BaseModel):
|
||||||
"""The user-facing error message."""
|
"""The user-facing error message."""
|
||||||
developer_message: str | None = None
|
developer_message: str | None = None
|
||||||
"""The developer-facing error details."""
|
"""The developer-facing error details."""
|
||||||
|
can_retry: bool = False
|
||||||
|
"""Whether the tool call can be retried."""
|
||||||
additional_prompt_content: str | None = None
|
additional_prompt_content: str | None = None
|
||||||
|
"""Additional content to be included in the retry prompt."""
|
||||||
|
retry_after_ms: int | None = None
|
||||||
|
"""The number of milliseconds (if any) to wait before retrying the tool call."""
|
||||||
|
|
||||||
|
|
||||||
class ToolCallOutput(BaseModel):
|
class ToolCallOutput(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,19 @@
|
||||||
"developer_message": {
|
"developer_message": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "An internal message that will be logged but will not be shown to the user or the AI model"
|
"description": "An internal message that will be logged but will not be shown to the user or the AI model"
|
||||||
|
},
|
||||||
|
"can_retry": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the tool call can be retried",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"additional_prompt_content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Additional content to be included in the retry prompt"
|
||||||
|
},
|
||||||
|
"retry_after_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The number of milliseconds (if any) to wait before retrying the tool call"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["message"],
|
"required": ["message"],
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,7 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"enum": ["oauth2", "github_app"]
|
|
||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import time
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from arcade.core.errors import ToolExecutionError, RetryableToolError
|
from arcade.core.errors import ToolExecutionError, RetryableToolError
|
||||||
from arcade.core.schema import ToolContext
|
from arcade.core.schema import ToolContext
|
||||||
|
|
@ -10,7 +9,16 @@ from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
requires_auth=SlackUser(
|
requires_auth=SlackUser(
|
||||||
scope=["chat:write", "im:write", "users.profile:read", "users:read"],
|
# TODO reduce this to chat:write, im:write, users.profile:read, users:read
|
||||||
|
# when incremental auth works
|
||||||
|
scope=[
|
||||||
|
"chat:write",
|
||||||
|
"im:write",
|
||||||
|
"users.profile:read",
|
||||||
|
"users:read",
|
||||||
|
"channels:read",
|
||||||
|
"groups:read",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def send_dm_to_user(
|
def send_dm_to_user(
|
||||||
|
|
@ -32,16 +40,11 @@ def send_dm_to_user(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
# does this end up as a developerMessage?
|
|
||||||
# does it end up in the LLM context?
|
|
||||||
# provide the dev an Error type that controls what ends up in the LLM context
|
|
||||||
|
|
||||||
# TODO make the sleep configurable and sent to the engine
|
|
||||||
time.sleep(0.5) # Wait for half a second
|
|
||||||
raise RetryableToolError(
|
raise RetryableToolError(
|
||||||
"User not found",
|
"User not found",
|
||||||
developer_message=f"User with username '{user_name}' not found.",
|
developer_message=f"User with username '{user_name}' not found.",
|
||||||
additional_prompt_content=format_users(userListResponse),
|
additional_prompt_content=format_users(userListResponse),
|
||||||
|
retry_after_ms=500, # Play nice with Slack API rate limits
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 2: Retrieve the DM channel ID with the user
|
# Step 2: Retrieve the DM channel ID with the user
|
||||||
|
|
@ -52,26 +55,35 @@ def send_dm_to_user(
|
||||||
slackClient.chat_postMessage(channel=dm_channel_id, text=message)
|
slackClient.chat_postMessage(channel=dm_channel_id, text=message)
|
||||||
|
|
||||||
except SlackApiError as e:
|
except SlackApiError as e:
|
||||||
|
error_message = e.response["error"] if "error" in e.response else str(e)
|
||||||
raise ToolExecutionError(
|
raise ToolExecutionError(
|
||||||
f"Error sending message: {e.response['error']}",
|
"Error sending message",
|
||||||
developer_message="Error sending message",
|
developer_message=f"Slack API Error: {error_message}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_users(userListResponse: dict) -> str:
|
def format_users(userListResponse: dict) -> str:
|
||||||
csv_string = "All active Slack users:\n\nid,name,real_name\n"
|
csv_string = "All active Slack users:\n\nname,real_name\n"
|
||||||
for user in userListResponse["members"]:
|
for user in userListResponse["members"]:
|
||||||
if not user.get("deleted", False):
|
if not user.get("deleted", False):
|
||||||
user_id = user.get("id", "")
|
|
||||||
name = user.get("name", "")
|
name = user.get("name", "")
|
||||||
real_name = user.get("profile", {}).get("real_name", "")
|
real_name = user.get("profile", {}).get("real_name", "")
|
||||||
csv_string += f"{user_id},{name},{real_name}\n"
|
csv_string += f"{name},{real_name}\n"
|
||||||
return csv_string.strip()
|
return csv_string.strip()
|
||||||
|
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
requires_auth=SlackUser(
|
requires_auth=SlackUser(
|
||||||
scope=["chat:write", "channels:read", "groups:read"],
|
# TODO reduce this to chat:write, channels:read, groups:read
|
||||||
|
# when incremental auth works
|
||||||
|
scope=[
|
||||||
|
"chat:write",
|
||||||
|
"im:write",
|
||||||
|
"users.profile:read",
|
||||||
|
"users:read",
|
||||||
|
"channels:read",
|
||||||
|
"groups:read",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def send_message_to_channel(
|
def send_message_to_channel(
|
||||||
|
|
@ -95,28 +107,28 @@ def send_message_to_channel(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not channel_id:
|
if not channel_id:
|
||||||
time.sleep(0.5) # Wait for half a second
|
|
||||||
raise RetryableToolError(
|
raise RetryableToolError(
|
||||||
"Channel not found",
|
"Channel not found",
|
||||||
developer_message=f"Channel with name '{channel_name}' not found.",
|
developer_message=f"Channel with name '{channel_name}' not found.",
|
||||||
additional_prompt_content=format_channels(channels_response),
|
additional_prompt_content=format_channels(channels_response),
|
||||||
|
retry_after_ms=500, # Play nice with Slack API rate limits
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 2: Send the message to the channel
|
# Step 2: Send the message to the channel
|
||||||
slackClient.chat_postMessage(channel=channel_id, text=message)
|
slackClient.chat_postMessage(channel=channel_id, text=message)
|
||||||
|
|
||||||
except SlackApiError as e:
|
except SlackApiError as e:
|
||||||
|
error_message = e.response["error"] if "error" in e.response else str(e)
|
||||||
raise ToolExecutionError(
|
raise ToolExecutionError(
|
||||||
f"Error sending message: {e.response['error']}",
|
"Error sending message",
|
||||||
developer_message="Error sending message",
|
developer_message=f"Slack API Error: {error_message}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_channels(channels_response: dict) -> str:
|
def format_channels(channels_response: dict) -> str:
|
||||||
csv_string = "All active Slack channels:\n\nid,name\n"
|
csv_string = "All active Slack channels:\n\nname\n"
|
||||||
for channel in channels_response["channels"]:
|
for channel in channels_response["channels"]:
|
||||||
if not channel.get("is_archived", False):
|
if not channel.get("is_archived", False):
|
||||||
channel_id = channel.get("id", "")
|
|
||||||
name = channel.get("name", "")
|
name = channel.get("name", "")
|
||||||
csv_string += f"{channel_id},{name}\n"
|
csv_string += f"{name}\n"
|
||||||
return csv_string.strip()
|
return csv_string.strip()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue