crewAI icon indicating copy to clipboard operation
crewAI copied to clipboard

[FEATURE] Improve agent/task templating

Open fmatray opened this issue 7 months ago • 7 comments

Feature Area

Core functionality

Is your feature request related to a an existing bug? Please link it here.

Today, agents and tasks yaml file only support basic replacement '{variable}' with few object types: int, float, int and bool.

It would be interesting to have more options, like:

  • Containers: List, Dict, Set, etc.
  • Standard objects: datetime, time, etc.
  • Custom objects: MyCustomObject

It would also nice to have more control, with if and loop statements, also with filtering options.

Of course, it's possible to do so doing before passing inputs. However, it mixes the logic and data.

This could help to make more flexible agents, tasks and crew.

Describe the solution you'd like

An easy way, would be to implement a library like Jinja2 as a templating tool:

  • It's robust and well known library and easy to implement
  • it give a lot of control by having a clear separation between app's logic and agent/task templating

The main downside, is the incompatibility between action variables '{var}' and jinja ones {{var}}.

Describe alternatives you've considered

In src/crewai/utilities/string_utils.py, instead of validating with validate_type(), just to str(variable) with a try/except to handle errors. As this can be an easy solution for some types like datetime, it can be more difficult with some kind of objects.

Moreover, it doesn't give any another control.

Additional context

No response

Willingness to Contribute

I can test the feature once it's implemented

fmatray avatar Apr 20 '25 14:04 fmatray

Can you share an example, I am interested to understand where exactly you are trying to use these

Containers: List, Dict, Set, etc.
Standard objects: datetime, time, etc.
Custom objects: MyCustomObject

Vidit-Ostwal avatar Apr 20 '25 14:04 Vidit-Ostwal

Let's start basic. I want a to pass a date (datetime) to a task. Now I have to do something like:

now = datetime.now()
start_date = now.strftime("%d/%M/%Y")
MyCrew.crew.kickoff(inputs={'start_date':  start_date})

and in my task:

  description:>
    analyse from {start_date}...

Now if I need just the year, I have to add another variable.

now = datetime.now()
start_date = now.strftime("%d/%M/%Y")
year = now.year
MyCrew.crew.kickoff(inputs={'start_date':  start_date, 'year': year})

and in my task:

  description:>
    analyse from {start_date}...
    Only focus on year {year}

It would be easier to have:

MyCrew.crew.kickoff(inputs={'now':datetime.now()})

and in my task:

  description:>
    analyse from {{now.strftime("%d/%M/%Y")}}...
    Only focus on year {now.year}...

Another example. let's sayI have a complex structured pydantic result from a research crew:

class Result(BaseModel)
  title: str
  summary: str
  key_points: List[str]
  data: List[tuple[int, int]] -> data to represent some useful graph
  key_dates: List[tuple[str, str]] -> [{'04-05-2023', 'A new breakthrough in AI Agent' }]
  sources: List[Dict[str, str]]. -> [{'title': "News article", 'url': 'http://...', 'publication_date': '01-02-2024', author: 'A great team'}, ...]

Now I want to pass the result to another another crew to process, I have to do something like:

def result_to_str(result:Result) -> str:
  ret = f"# {result.title}\n"
  ret +=  f"summary\n\n"
  ret += "Key points: \n"

  for key_point in  key_points:
    ret += f"  - {key_point}\n"

  for key_date in key_dates:
     ret += FORMAT_KEY_DATES
  ...
  return ret


ResultAnalyserCrew.crew.kickoff(inputs={'result': result_to_str(result_form_research_crew)})

task.yaml:

task:
  description: >
    analyse the result of ...

   ### Use this result
   {result}

If I need a part of the result in one task and another one in another task, it ends with two custom_to_str() functions, mixing code and AI instructions. This not flexible and not clean. That's why I propose a more kind of "Model View Controler" approach, very much the web framework philosophy like Django.

With a template engine, we can have a clean code:

ResultAnalyserCrew.crew.kickoff(inputs={'result': result_form_research_crew})

task.yaml:

generate_report_task:
  description: >
    analyse the result of ...

   ### Use this result
   # {{result.title}}
   {{result.summary}}

   Key points: 
   {{% result.key_points|sort  %}}
     - {{key_point}}
  {{% endfor%}}
 
generate_timeline_task:
  description: >
   generate a timeline with the key dates...

   ### key dates
   {{% for item in result.key_dates %}}
     - {{item[0]}}: {{item[1]}}
  {{% endfor%}}

generate_chart_task:
  description: >
   generate a nice chart using the tool SomeChartGeneratorTool with the following data...

   ### chart data
   {{% for data in result.data %}}
     - x={{data[0]}}, y={{data[1]}}
  {{% endfor%}}

This is just some basic examples to help to imagine some possibilities like more generic agents and tasks and handle complex objects from end to end. With Jinja2, we can profit of builtin filters like sum, replace, join, etc. without having to do in the python code.

I used jinja2 as an example as it's one of the most known template library in python, but perhaps another one fits better. As YAML offers possibilities, it does not offer placeholders and flow control.

fmatray avatar Apr 20 '25 16:04 fmatray

Another example. Now, if we need to pass details about a company, with have to use a flat dict.

Already existing object:

class Company:
  name:str
  sector:str
  location:str
  stock:str
  website:str

...
company = Company("Google", "Web search", "USA", "GOOGL", "www.google.com")

We need to flatten with just some "glue code" without any functionality.

inputs = dict(
    company_name=company.name,
    company_sector=company.sector,
    company_location=company.location,
    company_stock=company.stock,
    company_website=company.website,

    audience="Investors",
    analyse_period=5,
    report_language="German",
)
AnalysisFlow().kickoff(inputs=inputs)

It would be better to do:

inputs = dict(
    company= company
    audience="Investors",
    analyse_period=5,
    report_language="German",
)
AnalysisFlow().kickoff(inputs=inputs)

It doesn't change a lot for yaml file.

agents.yaml

analyst:
  role: >
    Expert in {{command.sector}} for {{audience}}...
  ... 

tasks.yaml

analyst_task:
  description: >
    Search on internet about {{company.name}}, located in {{company.location}}...

stock_analyst_task:
  description:>
    Evaluate {{company.name}} actions {{company.stock}} for the last {{analyse_period}}...
    report in {{report_language}}

If Company needs a new attribute (let's say "address"), only the object and the agent/task needs to be updated. The object doesn't need to be flattened in a large dict. The code is cleaner with less maintenance. Specially CrewAI is suppose to be integrated in a larger app with already existing objects.

fmatray avatar Apr 20 '25 18:04 fmatray

@fmatray love it, especially about structured data. Would be a great upgrade on our config engine

I’m just complaining about standard and complex objects. They might be a branch to execute malicious code in your crew. however I'd love to discuss it better

lucasgomide avatar Apr 24 '25 20:04 lucasgomide

I understand, security is an important point.

Jinja2, like many other templating engines, are safe by default. Objects' attributes are not evaluated (eval()) but transformed to strings. These libraries are widely use for web apps, so you can imagine how much security is important for developers.

The only way to have malicious execution would occur if, on purpose, an application developper remove safe or security options. This would be weird and on his responsibility.

To complete, as mentioned in the comment of the commit, I would avoid mixing the current system with another templating engine. This could lead to difficulties in debugging, random results and potential issues. I would rather totally implement the engine and remove the old one with a major update. Or keep both and switch with a global config options.

What do you think?

fmatray avatar Apr 25 '25 10:04 fmatray

I agree with create another system engine and then we can softly deprecated the current one in the future.

Regarding to our options, I liked the jinja but would be great a "spike task" to double checking other options.. I mean, Jinja2 is amazing but do we really need all that power

cc @lorenzejay @gvieira

lucasgomide avatar Apr 25 '25 12:04 lucasgomide

I agree with you., Jinja is a good engine. I don't know other ones except the django one. But both are very Web/HTML oriented.

fmatray avatar Apr 25 '25 13:04 fmatray

This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] avatar May 26 '25 12:05 github-actions[bot]

This issue was closed because it has been stalled for 5 days with no activity.

github-actions[bot] avatar Jun 01 '25 12:06 github-actions[bot]