mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
Reliability improvements (#77)
* fixing identation for AgentTools * updating gitignore to exclude quick test script * startingprompt translation * supporting individual task output * adding agent to task output * cutting new version * Updating README example
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ __pycache__
|
|||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
assets/*
|
assets/*
|
||||||
.idea
|
.idea
|
||||||
|
test.py
|
||||||
44
README.md
44
README.md
@@ -44,18 +44,14 @@ pip install duckduckgo-search
|
|||||||
import os
|
import os
|
||||||
from crewai import Agent, Task, Crew, Process
|
from crewai import Agent, Task, Crew, Process
|
||||||
|
|
||||||
|
os.environ["OPENAI_API_KEY"] = "YOUR KEY"
|
||||||
|
|
||||||
# You can choose to use a local model through Ollama for example.
|
# You can choose to use a local model through Ollama for example.
|
||||||
# In this case we will use OpenHermes 2.5 as an example.
|
|
||||||
#
|
#
|
||||||
# from langchain.llms import Ollama
|
# from langchain.llms import Ollama
|
||||||
# ollama_llm = Ollama(model="openhermes")
|
# ollama_llm = Ollama(model="openhermes")
|
||||||
|
|
||||||
# If you are using an ollama like above you don't need to set OPENAI_API_KEY.
|
|
||||||
os.environ["OPENAI_API_KEY"] = "Your Key"
|
|
||||||
|
|
||||||
# Define your tools, custom or not.
|
|
||||||
# Install duckduckgo-search for this example:
|
# Install duckduckgo-search for this example:
|
||||||
#
|
|
||||||
# !pip install -U duckduckgo-search
|
# !pip install -U duckduckgo-search
|
||||||
|
|
||||||
from langchain.tools import DuckDuckGoSearchRun
|
from langchain.tools import DuckDuckGoSearchRun
|
||||||
@@ -65,41 +61,46 @@ search_tool = DuckDuckGoSearchRun()
|
|||||||
researcher = Agent(
|
researcher = Agent(
|
||||||
role='Senior Research Analyst',
|
role='Senior Research Analyst',
|
||||||
goal='Uncover cutting-edge developments in AI and data science in',
|
goal='Uncover cutting-edge developments in AI and data science in',
|
||||||
backstory="""You are a Senior Research Analyst at a leading tech think tank.
|
backstory="""You work at a leading tech think tank.
|
||||||
Your expertise lies in identifying emerging trends and technologies in AI and
|
Your expertise lies in identifying emerging trends.
|
||||||
data science. You have a knack for dissecting complex data and presenting
|
You have a knack for dissecting complex data and presenting
|
||||||
actionable insights.""",
|
actionable insights.""",
|
||||||
verbose=True,
|
verbose=True,
|
||||||
allow_delegation=False,
|
allow_delegation=False,
|
||||||
tools=[search_tool]
|
tools=[search_tool]
|
||||||
# (optional) llm=ollama_llm, If you wanna use a local modal through Ollama, default is GPT4 with temperature=0.7
|
# You can pass an optional llm attribute specifying what mode you wanna use.
|
||||||
|
# It can be a local model through Ollama / LM Studio or a remote
|
||||||
|
# model like OpenAI, Mistral, Antrophic of others (https://python.langchain.com/docs/integrations/llms/)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# llm=ollama_llm # was defined above in the file
|
||||||
|
# llm=ChatOpenAI(model_name="gpt-3.5", temperature=0.7)
|
||||||
)
|
)
|
||||||
writer = Agent(
|
writer = Agent(
|
||||||
role='Tech Content Strategist',
|
role='Tech Content Strategist',
|
||||||
goal='Craft compelling content on tech advancements',
|
goal='Craft compelling content on tech advancements',
|
||||||
backstory="""You are a renowned Tech Content Strategist, known for your insightful
|
backstory="""You are a renowned Content Strategist, known for
|
||||||
and engaging articles on technology and innovation. With a deep understanding of
|
your insightful and engaging articles.
|
||||||
the tech industry, you transform complex concepts into compelling narratives.""",
|
You transform complex concepts into compelling narratives.""",
|
||||||
verbose=True,
|
verbose=True,
|
||||||
# (optional) llm=ollama_llm, If you wanna use a local modal through Ollama, default is GPT4 with temperature=0.7
|
allow_delegation=True,
|
||||||
allow_delegation=True
|
# (optional) llm=ollama_llm
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create tasks for your agents
|
# Create tasks for your agents
|
||||||
task1 = Task(
|
task1 = Task(
|
||||||
description="""Conduct a comprehensive analysis of the latest advancements in AI in 2024.
|
description="""Conduct a comprehensive analysis of the latest advancements in AI in 2024.
|
||||||
Identify key trends, breakthrough technologies, and potential industry impacts.
|
Identify key trends, breakthrough technologies, and potential industry impacts.
|
||||||
Compile your findings in a detailed report. Your final answer MUST be a full analysis report""",
|
Your final answer MUST be a full analysis report""",
|
||||||
agent=researcher
|
agent=researcher
|
||||||
)
|
)
|
||||||
|
|
||||||
task2 = Task(
|
task2 = Task(
|
||||||
description="""Using the insights from the researcher's report, develop an engaging blog
|
description="""Using the insights provided, develop an engaging blog
|
||||||
post that highlights the most significant AI advancements.
|
post that highlights the most significant AI advancements.
|
||||||
Your post should be informative yet accessible, catering to a tech-savvy audience.
|
Your post should be informative yet accessible, catering to a tech-savvy audience.
|
||||||
Aim for a narrative that captures the essence of these breakthroughs and their
|
Make it sound cool, avoid complex words so it doesn't sound like AI.
|
||||||
implications for the future. Your final answer MUST be the full blog post of at least 3 paragraphs.""",
|
Your final answer MUST be the full blog post of at least 4 paragraphs.""",
|
||||||
agent=writer
|
agent=writer
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,8 +108,7 @@ task2 = Task(
|
|||||||
crew = Crew(
|
crew = Crew(
|
||||||
agents=[researcher, writer],
|
agents=[researcher, writer],
|
||||||
tasks=[task1, task2],
|
tasks=[task1, task2],
|
||||||
verbose=2, # Crew verbose more will let you know what tasks are being worked on, you can set it to 1 or 2 to different logging levels
|
verbose=2, # You can set it to 1 or 2 to different logging levels
|
||||||
process=Process.sequential # Sequential process will have tasks executed one after the other and the outcome of the previous one is passed as extra content into this next.
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get your crew to work!
|
# Get your crew to work!
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ class Agent(BaseModel):
|
|||||||
)
|
)
|
||||||
executor_args["memory"] = summary_memory
|
executor_args["memory"] = summary_memory
|
||||||
agent_args["chat_history"] = lambda x: x["chat_history"]
|
agent_args["chat_history"] = lambda x: x["chat_history"]
|
||||||
prompt = Prompts.TASK_EXECUTION_WITH_MEMORY_PROMPT
|
prompt = Prompts().task_execution_with_memory()
|
||||||
else:
|
else:
|
||||||
prompt = Prompts.TASK_EXECUTION_PROMPT
|
prompt = Prompts().task_execution()
|
||||||
|
|
||||||
execution_prompt = prompt.partial(
|
execution_prompt = prompt.partial(
|
||||||
goal=self.goal,
|
goal=self.goal,
|
||||||
|
|||||||
@@ -111,21 +111,21 @@ class Crew(BaseModel):
|
|||||||
Returns:
|
Returns:
|
||||||
Output of the crew.
|
Output of the crew.
|
||||||
"""
|
"""
|
||||||
task_outcome = None
|
task_output = None
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
# Add delegation tools to the task if the agent allows it
|
# Add delegation tools to the task if the agent allows it
|
||||||
if task.agent.allow_delegation:
|
if task.agent.allow_delegation:
|
||||||
tools = AgentTools(agents=self.agents).tools()
|
agent_tools = AgentTools(agents=self.agents).tools()
|
||||||
task.tools += tools
|
task.tools += agent_tools
|
||||||
|
|
||||||
self.__log("debug", f"Working Agent: {task.agent.role}")
|
self.__log("debug", f"Working Agent: {task.agent.role}")
|
||||||
self.__log("info", f"Starting Task: {task.description} ...")
|
self.__log("info", f"Starting Task: {task.description}")
|
||||||
|
|
||||||
task_outcome = task.execute(task_outcome)
|
task_output = task.execute(task_output)
|
||||||
|
self.__log(
|
||||||
self.__log("debug", f"Task output: {task_outcome}")
|
"debug", f"\n\n[{task.agent.role}] Task output: {task_output}\n\n"
|
||||||
|
)
|
||||||
return task_outcome
|
return task_output
|
||||||
|
|
||||||
def __log(self, level, message):
|
def __log(self, level, message):
|
||||||
"""Log a message"""
|
"""Log a message"""
|
||||||
|
|||||||
@@ -1,84 +1,53 @@
|
|||||||
"""Prompts for generic agent."""
|
"""Prompts for generic agent."""
|
||||||
|
import json
|
||||||
from textwrap import dedent
|
import os
|
||||||
from typing import ClassVar
|
from typing import ClassVar, Dict, Optional
|
||||||
|
|
||||||
from langchain.prompts import PromptTemplate
|
from langchain.prompts import PromptTemplate
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||||
|
|
||||||
|
|
||||||
class Prompts(BaseModel):
|
class Prompts(BaseModel):
|
||||||
"""Prompts for generic agent."""
|
"""Prompts for generic agent."""
|
||||||
|
|
||||||
TASK_SLICE: ClassVar[str] = dedent(
|
_prompts: Optional[Dict[str, str]] = PrivateAttr()
|
||||||
"""\
|
language: Optional[str] = Field(
|
||||||
Begin! This is VERY important to you, your job depends on it!
|
default="en",
|
||||||
|
description="Language of crewai prompts.",
|
||||||
Current Task: {input}"""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def load_prompts(self) -> "Prompts":
|
||||||
|
"""Load prompts from file."""
|
||||||
|
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
prompts_path = os.path.join(dir_path, f"prompts/{self.language}.json")
|
||||||
|
|
||||||
|
with open(prompts_path, "r") as f:
|
||||||
|
self._prompts = json.load(f)["slices"]
|
||||||
|
return self
|
||||||
|
|
||||||
SCRATCHPAD_SLICE: ClassVar[str] = "\n{agent_scratchpad}"
|
SCRATCHPAD_SLICE: ClassVar[str] = "\n{agent_scratchpad}"
|
||||||
|
|
||||||
MEMORY_SLICE: ClassVar[str] = dedent(
|
def task_execution_with_memory(self) -> str:
|
||||||
"""\
|
return PromptTemplate.from_template(
|
||||||
This is the summary of your work so far:
|
self._prompts["role_playing"]
|
||||||
{chat_history}"""
|
+ self._prompts["tools"]
|
||||||
)
|
+ self._prompts["memory"]
|
||||||
|
+ self._prompts["task"]
|
||||||
|
+ self.SCRATCHPAD_SLICE
|
||||||
|
)
|
||||||
|
|
||||||
ROLE_PLAYING_SLICE: ClassVar[str] = dedent(
|
def task_execution_without_tools(self) -> str:
|
||||||
"""\
|
return PromptTemplate.from_template(
|
||||||
You are {role}.
|
self._prompts["role_playing"]
|
||||||
{backstory}
|
+ self._prompts["task"]
|
||||||
|
+ self.SCRATCHPAD_SLICE
|
||||||
|
)
|
||||||
|
|
||||||
Your personal goal is: {goal}"""
|
def task_execution(self) -> str:
|
||||||
)
|
return PromptTemplate.from_template(
|
||||||
|
self._prompts["role_playing"]
|
||||||
TOOLS_SLICE: ClassVar[str] = dedent(
|
+ self._prompts["tools"]
|
||||||
"""\
|
+ self._prompts["task"]
|
||||||
|
+ self.SCRATCHPAD_SLICE
|
||||||
|
)
|
||||||
TOOLS:
|
|
||||||
------
|
|
||||||
You have access to the following tools:
|
|
||||||
|
|
||||||
{tools}
|
|
||||||
|
|
||||||
To use a tool, please use the exact following format:
|
|
||||||
|
|
||||||
```
|
|
||||||
Thought: Do I need to use a tool? Yes
|
|
||||||
Action: the action to take, should be one of [{tool_names}], just the name.
|
|
||||||
Action Input: the input to the action
|
|
||||||
Observation: the result of the action
|
|
||||||
```
|
|
||||||
|
|
||||||
When you have a response for your task, or if you do not need to use a tool, you MUST use the format:
|
|
||||||
|
|
||||||
```
|
|
||||||
Thought: Do I need to use a tool? No
|
|
||||||
Final Answer: [your response here]
|
|
||||||
```"""
|
|
||||||
)
|
|
||||||
|
|
||||||
VOTING_SLICE: ClassVar[str] = dedent(
|
|
||||||
"""\
|
|
||||||
You are working on a crew with your co-workers and need to decide who will execute the task.
|
|
||||||
|
|
||||||
These are your format instructions:
|
|
||||||
{format_instructions}
|
|
||||||
|
|
||||||
These are your co-workers and their roles:
|
|
||||||
{coworkers}"""
|
|
||||||
)
|
|
||||||
|
|
||||||
TASK_EXECUTION_WITH_MEMORY_PROMPT: ClassVar[str] = PromptTemplate.from_template(
|
|
||||||
ROLE_PLAYING_SLICE + TOOLS_SLICE + MEMORY_SLICE + TASK_SLICE + SCRATCHPAD_SLICE
|
|
||||||
)
|
|
||||||
|
|
||||||
TASK_EXECUTION_PROMPT: ClassVar[str] = PromptTemplate.from_template(
|
|
||||||
ROLE_PLAYING_SLICE + TOOLS_SLICE + TASK_SLICE + SCRATCHPAD_SLICE
|
|
||||||
)
|
|
||||||
|
|
||||||
CONSENSUNS_VOTING_PROMPT: ClassVar[str] = PromptTemplate.from_template(
|
|
||||||
ROLE_PLAYING_SLICE + VOTING_SLICE + TASK_SLICE + SCRATCHPAD_SLICE
|
|
||||||
)
|
|
||||||
|
|||||||
8
crewai/prompts/en.json
Normal file
8
crewai/prompts/en.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"slices": {
|
||||||
|
"task": "Begin! This is VERY important to you, your job depends on it!\n\nCurrent Task: {input}",
|
||||||
|
"memory": "This is the summary of your work so far:\n{chat_history}",
|
||||||
|
"role_playing": "You are {role}.\n{backstory}\n\nYour personal goal is: {goal}",
|
||||||
|
"tools": "TOOLS:\n------\nYou have access to the following tools:\n\n{tools}\n\nTo use a tool, please use the exact following format:\n\n```\nThought: Do I need to use a tool? Yes\nAction: the action to take, should be one of [{tool_names}], just the name.\nAction Input: the input to the action\nObservation: the result of the action\n```\n\nWhen you have a response for your task, or if you do not need to use a tool, you MUST use the format:\n\n```\nThought: Do I need to use a tool? No\nFinal Answer: [your response here]"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ from pydantic import UUID4, BaseModel, Field, field_validator, model_validator
|
|||||||
from pydantic_core import PydanticCustomError
|
from pydantic_core import PydanticCustomError
|
||||||
|
|
||||||
from crewai.agent import Agent
|
from crewai.agent import Agent
|
||||||
|
from crewai.tasks.task_output import TaskOutput
|
||||||
|
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
@@ -19,6 +20,9 @@ class Task(BaseModel):
|
|||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Tools the agent are limited to use for this task.",
|
description="Tools the agent are limited to use for this task.",
|
||||||
)
|
)
|
||||||
|
output: Optional[TaskOutput] = Field(
|
||||||
|
description="Task output, it's final result.", default=None
|
||||||
|
)
|
||||||
id: UUID4 = Field(
|
id: UUID4 = Field(
|
||||||
default_factory=uuid.uuid4,
|
default_factory=uuid.uuid4,
|
||||||
frozen=True,
|
frozen=True,
|
||||||
@@ -46,9 +50,12 @@ class Task(BaseModel):
|
|||||||
Output of the task.
|
Output of the task.
|
||||||
"""
|
"""
|
||||||
if self.agent:
|
if self.agent:
|
||||||
return self.agent.execute_task(
|
result = self.agent.execute_task(
|
||||||
task=self.description, context=context, tools=self.tools
|
task=self.description, context=context, tools=self.tools
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.output = TaskOutput(description=self.description, result=result)
|
||||||
|
return result
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"The task '{self.description}' has no agent assigned, therefore it can't be executed directly and should be executed in a Crew using a specific process that support that, either consensual or hierarchical."
|
f"The task '{self.description}' has no agent assigned, therefore it can't be executed directly and should be executed in a Crew using a specific process that support that, either consensual or hierarchical."
|
||||||
|
|||||||
17
crewai/tasks/task_output.py
Normal file
17
crewai/tasks/task_output.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOutput(BaseModel):
|
||||||
|
"""Class that represents the result of a task."""
|
||||||
|
|
||||||
|
description: str = Field(description="Description of the task")
|
||||||
|
summary: Optional[str] = Field(description="Summary of the task", default=None)
|
||||||
|
result: str = Field(description="Result of the task")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def set_summary(self):
|
||||||
|
excerpt = " ".join(self.description.split(" ")[0:10])
|
||||||
|
self.summary = f"{excerpt}..."
|
||||||
|
return self
|
||||||
@@ -8,7 +8,7 @@ from crewai.agent import Agent
|
|||||||
|
|
||||||
|
|
||||||
class AgentTools(BaseModel):
|
class AgentTools(BaseModel):
|
||||||
"""Tools for generic agent."""
|
"""Default tools around agent delegation"""
|
||||||
|
|
||||||
agents: List[Agent] = Field(description="List of agents in this crew.")
|
agents: List[Agent] = Field(description="List of agents in this crew.")
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ class AgentTools(BaseModel):
|
|||||||
description=dedent(
|
description=dedent(
|
||||||
f"""\
|
f"""\
|
||||||
Useful to delegate a specific task to one of the
|
Useful to delegate a specific task to one of the
|
||||||
following co-workers: [{', '.join([agent.role for agent in self.agents])}].
|
following co-workers: [{', '.join([agent.role for agent in self.agents])}].
|
||||||
The input to this tool should be a pipe (|) separated text of length
|
The input to this tool should be a pipe (|) separated text of length
|
||||||
three, representing the co-worker you want to ask it to (one of the options),
|
three, representing the co-worker you want to ask it to (one of the options),
|
||||||
the task and all actual context you have for the task.
|
the task and all actual context you have for the task.
|
||||||
For example, `coworker|task|context`.
|
For example, `coworker|task|context`.
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tool.from_function(
|
Tool.from_function(
|
||||||
@@ -34,12 +34,12 @@ class AgentTools(BaseModel):
|
|||||||
description=dedent(
|
description=dedent(
|
||||||
f"""\
|
f"""\
|
||||||
Useful to ask a question, opinion or take from on
|
Useful to ask a question, opinion or take from on
|
||||||
of the following co-workers: [{', '.join([agent.role for agent in self.agents])}].
|
of the following co-workers: [{', '.join([agent.role for agent in self.agents])}].
|
||||||
The input to this tool should be a pipe (|) separated text of length
|
The input to this tool should be a pipe (|) separated text of length
|
||||||
three, representing the co-worker you want to ask it to (one of the options),
|
three, representing the co-worker you want to ask it to (one of the options),
|
||||||
the question and all actual context you have for the question.
|
the question and all actual context you have for the question.
|
||||||
For example, `coworker|question|context`.
|
For example, `coworker|question|context`.
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.1.16"
|
version = "0.1.23"
|
||||||
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
||||||
authors = ["Joao Moura <joaomdmoura@gmail.com>"]
|
authors = ["Joao Moura <joaomdmoura@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -180,11 +180,11 @@ def test_crew_verbose_output(capsys):
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
expected_strings = [
|
expected_strings = [
|
||||||
"Working Agent: Researcher",
|
"Working Agent: Researcher",
|
||||||
"Starting Task: Research AI advancements. ...",
|
"Starting Task: Research AI advancements.",
|
||||||
"Task output:",
|
"[Researcher] Task output:",
|
||||||
"Working Agent: Senior Writer",
|
"Working Agent: Senior Writer",
|
||||||
"Starting Task: Write about AI in healthcare. ...",
|
"Starting Task: Write about AI in healthcare.",
|
||||||
"Task output:",
|
"[Senior Writer] Task output:",
|
||||||
]
|
]
|
||||||
|
|
||||||
for expected_string in expected_strings:
|
for expected_string in expected_strings:
|
||||||
@@ -205,7 +205,7 @@ def test_crew_verbose_levels_output(capsys):
|
|||||||
|
|
||||||
crew.kickoff()
|
crew.kickoff()
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
expected_strings = ["Working Agent: Researcher", "Task output:"]
|
expected_strings = ["Working Agent: Researcher", "[Researcher] Task output:"]
|
||||||
|
|
||||||
for expected_string in expected_strings:
|
for expected_string in expected_strings:
|
||||||
assert expected_string in captured.out
|
assert expected_string in captured.out
|
||||||
@@ -216,8 +216,8 @@ def test_crew_verbose_levels_output(capsys):
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
expected_strings = [
|
expected_strings = [
|
||||||
"Working Agent: Researcher",
|
"Working Agent: Researcher",
|
||||||
"Starting Task: Write about AI advancements. ...",
|
"Starting Task: Write about AI advancements.",
|
||||||
"Task output:",
|
"[Researcher] Task output:",
|
||||||
]
|
]
|
||||||
|
|
||||||
for expected_string in expected_strings:
|
for expected_string in expected_strings:
|
||||||
|
|||||||
Reference in New Issue
Block a user