diff --git a/README.md b/README.md index d57877707..3d82661b5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ We're excited to welcome new contributors to the Julep project! We've created several "good first issues" to help you get started. Here's how you can contribute: -1. Check out our [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute. +1. Check out our [CONTRIBUTING.md](https://github.com/julep-ai/julep/blob/dev/CONTRIBUTING.md) file for guidelines on how to contribute. 2. Browse our [good first issues](https://github.com/julep-ai/julep/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to find a task that interests you. 3. If you have any questions or need help, don't hesitate to reach out on our [Discord](https://discord.com/invite/JTSBGRZrzj) channel. diff --git a/agents-api/agents_api/activities/excecute_api_call.py b/agents-api/agents_api/activities/excecute_api_call.py index 88fabce89..e7752aa06 100644 --- a/agents-api/agents_api/activities/excecute_api_call.py +++ b/agents-api/agents_api/activities/excecute_api_call.py @@ -1,3 +1,4 @@ +import base64 from typing import Annotated, Any, Optional, TypedDict, Union import httpx @@ -20,6 +21,8 @@ class RequestArgs(TypedDict): json_: Optional[dict[str, Any]] cookies: Optional[dict[str, str]] params: Optional[Union[str, dict[str, Any]]] + url: Optional[str] + headers: Optional[dict[str, str]] @beartype @@ -29,18 +32,23 @@ async def execute_api_call( ) -> Any: try: async with httpx.AsyncClient() as client: + arg_url = request_args.pop("url", None) + arg_headers = request_args.pop("headers", None) + response = await client.request( method=api_call.method, - url=str(api_call.url), - headers=api_call.headers, + url=arg_url or str(api_call.url), + headers=arg_headers or api_call.headers, follow_redirects=api_call.follow_redirects, **request_args, ) + content_base64 = base64.b64encode(response.content).decode("ascii") + response_dict = { "status_code": response.status_code, "headers": dict(response.headers), - "content": response.content, + "content": content_base64, "json": response.json(), } diff --git a/agents-api/agents_api/activities/utils.py b/agents-api/agents_api/activities/utils.py index f9f7ded12..fca62578a 100644 --- a/agents-api/agents_api/activities/utils.py +++ b/agents-api/agents_api/activities/utils.py @@ -1,20 +1,33 @@ +import base64 +import datetime as dt +import functools +import itertools import json -from functools import reduce -from itertools import accumulate -from random import random -from time import time -from typing import Any, Callable +import math +import random +import statistics +import string +import time +import urllib.parse +from typing import Any, Callable, ParamSpec, Type, TypeVar, cast import re2 import yaml +import zoneinfo from beartype import beartype from simpleeval import EvalWithCompoundTypes, SimpleEval -from yaml import CSafeLoader +from yaml import CSafeDumper, CSafeLoader + +T = TypeVar("T") + + +P = ParamSpec("P") +R = TypeVar("R") + # TODO: We need to make sure that we dont expose any security issues ALLOWED_FUNCTIONS = { "abs": abs, - "accumulate": accumulate, "all": all, "any": any, "bool": bool, @@ -25,23 +38,169 @@ "int": int, "len": len, "list": list, - "load_json": json.loads, - "load_yaml": lambda string: yaml.load(string, Loader=CSafeLoader), "map": map, - "match_regex": lambda pattern, string: bool(re2.fullmatch(pattern, string)), "max": max, "min": min, - "random": random, "range": range, - "reduce": reduce, "round": round, - "search_regex": lambda pattern, string: re2.search(pattern, string), "set": set, "str": str, "sum": sum, - "time": time, "tuple": tuple, + "reduce": functools.reduce, "zip": zip, + "search_regex": lambda pattern, string: re2.search(pattern, string), + "load_json": json.loads, + "load_yaml": lambda string: yaml.load(string, Loader=CSafeLoader), + "match_regex": lambda pattern, string: bool(re2.fullmatch(pattern, string)), +} + + +class stdlib_re: + fullmatch = re2.fullmatch + search = re2.search + escape = re2.escape + findall = re2.findall + finditer = re2.finditer + match = re2.match + split = re2.split + sub = re2.sub + subn = re2.subn + + +class stdlib_json: + loads = json.loads + dumps = json.dumps + + +class stdlib_yaml: + load = lambda string: yaml.load(string, Loader=CSafeLoader) # noqa: E731 + dump = lambda value: yaml.dump(value, Dumper=CSafeDumper) # noqa: E731 + + +class stdlib_time: + strftime = time.strftime + strptime = time.strptime + time = time + + +class stdlib_random: + choice = random.choice + choices = random.choices + sample = random.sample + shuffle = random.shuffle + randrange = random.randrange + randint = random.randint + random = random.random + + +class stdlib_itertools: + accumulate = itertools.accumulate + + +class stdlib_functools: + partial = functools.partial + reduce = functools.reduce + + +class stdlib_base64: + b64encode = base64.b64encode + b64decode = base64.b64decode + + +class stdlib_urllib: + class parse: + urlparse = urllib.parse.urlparse + urlencode = urllib.parse.urlencode + unquote = urllib.parse.unquote + quote = urllib.parse.quote + parse_qs = urllib.parse.parse_qs + parse_qsl = urllib.parse.parse_qsl + urlsplit = urllib.parse.urlsplit + urljoin = urllib.parse.urljoin + unwrap = urllib.parse.unwrap + + +class stdlib_string: + ascii_letters = string.ascii_letters + ascii_lowercase = string.ascii_lowercase + ascii_uppercase = string.ascii_uppercase + digits = string.digits + hexdigits = string.hexdigits + octdigits = string.octdigits + punctuation = string.punctuation + whitespace = string.whitespace + printable = string.printable + + +class stdlib_zoneinfo: + ZoneInfo = zoneinfo.ZoneInfo + + +class stdlib_datetime: + class timezone: + class utc: + utc = dt.timezone.utc + + class datetime: + now = dt.datetime.now + datetime = dt.datetime + timedelta = dt.timedelta + date = dt.date + time = dt.time + + timedelta = dt.timedelta + + +class stdlib_math: + sqrt = math.sqrt + exp = math.exp + ceil = math.ceil + floor = math.floor + isinf = math.isinf + isnan = math.isnan + log = math.log + log10 = math.log10 + log2 = math.log2 + pow = math.pow + sin = math.sin + cos = math.cos + tan = math.tan + asin = math.asin + acos = math.acos + atan = math.atan + atan2 = math.atan2 + + pi = math.pi + e = math.e + + +class stdlib_statistics: + mean = statistics.mean + stdev = statistics.stdev + geometric_mean = statistics.geometric_mean + median = statistics.median + median_low = statistics.median_low + median_high = statistics.median_high + mode = statistics.mode + quantiles = statistics.quantiles + + +stdlib = { + "re": stdlib_re, + "json": stdlib_json, + "yaml": stdlib_yaml, + "time": stdlib_time, + "random": stdlib_random, + "itertools": stdlib_itertools, + "functools": stdlib_functools, + "base64": stdlib_base64, + "urllib": stdlib_urllib, + "string": stdlib_string, + "zoneinfo": stdlib_zoneinfo, + "datetime": stdlib_datetime, + "math": stdlib_math, + "statistics": stdlib_statistics, } @@ -50,7 +209,7 @@ def get_evaluator( names: dict[str, Any], extra_functions: dict[str, Callable] | None = None ) -> SimpleEval: evaluator = EvalWithCompoundTypes( - names=names, functions=ALLOWED_FUNCTIONS | (extra_functions or {}) + names=names | stdlib, functions=ALLOWED_FUNCTIONS | (extra_functions or {}) ) return evaluator diff --git a/agents-api/agents_api/common/exceptions/tasks.py b/agents-api/agents_api/common/exceptions/tasks.py index 8ead1e7e2..81331234c 100644 --- a/agents-api/agents_api/common/exceptions/tasks.py +++ b/agents-api/agents_api/common/exceptions/tasks.py @@ -20,7 +20,7 @@ import temporalio.exceptions # List of error types that should not be retried -NON_RETRYABLE_ERROR_TYPES = [ +NON_RETRYABLE_ERROR_TYPES = ( # Temporal-specific errors temporalio.exceptions.WorkflowAlreadyStartedError, temporalio.exceptions.TerminatedError, @@ -99,10 +99,10 @@ litellm.exceptions.ServiceUnavailableError, litellm.exceptions.OpenAIError, litellm.exceptions.APIError, -] +) -def is_non_retryable_error(error: Exception) -> bool: +def is_non_retryable_error(error: BaseException) -> bool: """ Determines if the given error is non-retryable. @@ -115,4 +115,4 @@ def is_non_retryable_error(error: Exception) -> bool: Returns: bool: True if the error is non-retryable, False otherwise. """ - return isinstance(error, tuple(NON_RETRYABLE_ERROR_TYPES)) + return isinstance(error, NON_RETRYABLE_ERROR_TYPES) diff --git a/agents-api/agents_api/common/interceptors.py b/agents-api/agents_api/common/interceptors.py index 2fb077c45..c6e8e2eaf 100644 --- a/agents-api/agents_api/common/interceptors.py +++ b/agents-api/agents_api/common/interceptors.py @@ -31,7 +31,7 @@ class CustomActivityInterceptor(ActivityInboundInterceptor): async def execute_activity(self, input: ExecuteActivityInput): try: return await super().execute_activity(input) - except Exception as e: + except BaseException as e: if is_non_retryable_error(e): raise ApplicationError( str(e), @@ -53,7 +53,7 @@ class CustomWorkflowInterceptor(WorkflowInboundInterceptor): async def execute_workflow(self, input: ExecuteWorkflowInput): try: return await super().execute_workflow(input) - except Exception as e: + except BaseException as e: if is_non_retryable_error(e): raise ApplicationError( str(e), diff --git a/agents-api/agents_api/common/protocol/tasks.py b/agents-api/agents_api/common/protocol/tasks.py index bbb5c28d3..bd4aaa5a2 100644 --- a/agents-api/agents_api/common/protocol/tasks.py +++ b/agents-api/agents_api/common/protocol/tasks.py @@ -118,7 +118,8 @@ } # type: ignore -PartialTransition: Type[BaseModel] = create_partial_model(CreateTransitionRequest) +class PartialTransition(create_partial_model(CreateTransitionRequest)): + user_state: dict[str, Any] = Field(default_factory=dict) class ExecutionInput(BaseModel): diff --git a/agents-api/agents_api/models/execution/create_execution_transition.py b/agents-api/agents_api/models/execution/create_execution_transition.py index f40395126..2b1c09ae8 100644 --- a/agents-api/agents_api/models/execution/create_execution_transition.py +++ b/agents-api/agents_api/models/execution/create_execution_transition.py @@ -176,7 +176,7 @@ def create_execution_transition( data=UpdateExecutionRequest( status=transition_to_execution_status[data.type] ), - output=data.output if data.type == "finish" else None, + output=data.output if data.type != "error" else None, error=str(data.output) if data.type == "error" and data.output else None, diff --git a/agents-api/agents_api/workflows/task_execution/__init__.py b/agents-api/agents_api/workflows/task_execution/__init__.py index edf54fb12..155b49397 100644 --- a/agents-api/agents_api/workflows/task_execution/__init__.py +++ b/agents-api/agents_api/workflows/task_execution/__init__.py @@ -118,30 +118,6 @@ # Main workflow definition @workflow.defn class TaskExecutionWorkflow: - user_state: dict[str, Any] = {} - - def __init__(self) -> None: - self.user_state = {} - - # TODO: Add endpoints for getting and setting user state for an execution - # Query methods for user state - @workflow.query - def get_user_state(self) -> dict[str, Any]: - return self.user_state - - @workflow.query - def get_user_state_by_key(self, key: str) -> Any: - return self.user_state.get(key) - - # Signal methods for updating user state - @workflow.signal - def set_user_state(self, key: str, value: Any) -> None: - self.user_state[key] = value - - @workflow.signal - def update_user_state(self, values: dict[str, Any]) -> None: - self.user_state.update(values) - # Main workflow run method @workflow.run async def run( @@ -149,11 +125,7 @@ async def run( execution_input: ExecutionInput, start: TransitionTarget = TransitionTarget(workflow="main", step=0), previous_inputs: list[Any] = [], - user_state: dict[str, Any] = {}, ) -> Any: - # Set the initial user state - self.user_state = user_state - workflow.logger.info( f"TaskExecutionWorkflow for task {execution_input.task.id}" f" [LOC {start.workflow}.{start.step}]" @@ -258,7 +230,6 @@ async def run( switch=switch, index=index, previous_inputs=previous_inputs, - user_state=self.user_state, ) state = PartialTransition(output=result) @@ -276,7 +247,6 @@ async def run( else_branch=else_branch, condition=condition, previous_inputs=previous_inputs, - user_state=self.user_state, ) state = PartialTransition(output=result) @@ -288,7 +258,6 @@ async def run( do_step=do_step, items=items, previous_inputs=previous_inputs, - user_state=self.user_state, ) state = PartialTransition(output=result) @@ -303,7 +272,6 @@ async def run( reduce=reduce, initial=initial, previous_inputs=previous_inputs, - user_state=self.user_state, ) state = PartialTransition(output=result) @@ -316,7 +284,6 @@ async def run( map_defn=map_defn, items=items, previous_inputs=previous_inputs, - user_state=self.user_state, initial=initial, reduce=reduce, parallelism=parallelism, @@ -376,7 +343,6 @@ async def run( context, start=yield_next_target, previous_inputs=[output], - user_state=self.user_state, ) state = PartialTransition(output=result) @@ -439,14 +405,15 @@ async def run( case SetStep(), StepOutcome(output=evaluated_output): workflow.logger.info("Set step: Updating user state") - self.update_user_state(evaluated_output) # Pass along the previous output unchanged - state = PartialTransition(output=context.current_input) + state = PartialTransition( + output=context.current_input, user_state=evaluated_output + ) case GetStep(get=key), _: workflow.logger.info(f"Get step: Fetching '{key}' from user state") - value = self.get_user_state_by_key(key) + value = workflow.memo_value(key, default=None) workflow.logger.debug(f"Retrieved value: {value}") state = PartialTransition(output=value) @@ -596,5 +563,5 @@ def model_dump(obj): context.execution_input, start=final_state.next, previous_inputs=previous_inputs + [final_state.output], - user_state=self.user_state, + user_state=state.user_state, ) diff --git a/agents-api/agents_api/workflows/task_execution/helpers.py b/agents-api/agents_api/workflows/task_execution/helpers.py index 04449db58..271f33dbf 100644 --- a/agents-api/agents_api/workflows/task_execution/helpers.py +++ b/agents-api/agents_api/workflows/task_execution/helpers.py @@ -27,15 +27,23 @@ async def continue_as_child( previous_inputs: list[Any], user_state: dict[str, Any] = {}, ) -> Any: - return await workflow.execute_child_workflow( - "TaskExecutionWorkflow", + info = workflow.info() + + if info.is_continue_as_new_suggested(): + run = workflow.continue_as_new + else: + run = lambda *args, **kwargs: workflow.execute_child_workflow( # noqa: E731 + info.workflow_type, *args, **kwargs + ) + + return await run( args=[ execution_input, start, previous_inputs, - user_state, ], retry_policy=DEFAULT_RETRY_POLICY, + memo=workflow.memo() | user_state, ) @@ -46,7 +54,7 @@ async def execute_switch_branch( switch: list, index: int, previous_inputs: list[Any], - user_state: dict[str, Any], + user_state: dict[str, Any] = {}, ) -> Any: workflow.logger.info(f"Switch step: Chose branch {index}") chosen_branch = switch[index] @@ -77,7 +85,7 @@ async def execute_if_else_branch( else_branch: WorkflowStep, condition: bool, previous_inputs: list[Any], - user_state: dict[str, Any], + user_state: dict[str, Any] = {}, ) -> Any: workflow.logger.info(f"If-Else step: Condition evaluated to {condition}") chosen_branch = then_branch if condition else else_branch @@ -108,7 +116,7 @@ async def execute_foreach_step( do_step: WorkflowStep, items: list[Any], previous_inputs: list[Any], - user_state: dict[str, Any], + user_state: dict[str, Any] = {}, ) -> Any: workflow.logger.info(f"Foreach step: Iterating over {len(items)} items") results = [] @@ -142,7 +150,7 @@ async def execute_map_reduce_step( map_defn: WorkflowStep, items: list[Any], previous_inputs: list[Any], - user_state: dict[str, Any], + user_state: dict[str, Any] = {}, reduce: str | None = None, initial: Any = [], ) -> Any: @@ -185,7 +193,7 @@ async def execute_map_reduce_step_parallel( map_defn: WorkflowStep, items: list[Any], previous_inputs: list[Any], - user_state: dict[str, Any], + user_state: dict[str, Any] = {}, initial: Any = [], reduce: str | None = None, parallelism: int = task_max_parallelism, diff --git a/agents-api/tests/test_execution_workflow.py b/agents-api/tests/test_execution_workflow.py index e5ef7110a..f8a89cb62 100644 --- a/agents-api/tests/test_execution_workflow.py +++ b/agents-api/tests/test_execution_workflow.py @@ -819,9 +819,9 @@ async def _( "input_schema": {"type": "object", "additionalProperties": True}, "main": [ { - "if": "True", + "if": "False", "then": {"evaluate": {"hello": '"world"'}}, - "else": {"evaluate": {"hello": '"nope"'}}, + "else": {"evaluate": {"hello": "random.randint(0, 10)"}}, }, ], } @@ -849,7 +849,7 @@ async def _( mock_run_task_execution_workflow.assert_called_once() result = await handle.result() - assert result["hello"] == "world" + assert result["hello"] in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @test("workflow: switch step") diff --git a/cookbooks/01-Website_Crawler_using_Spider.ipynb b/cookbooks/01-Website_Crawler_using_Spider.ipynb index 36a77d525..c65a3cb40 100644 --- a/cookbooks/01-Website_Crawler_using_Spider.ipynb +++ b/cookbooks/01-Website_Crawler_using_Spider.ipynb @@ -173,7 +173,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here is a Task which uses a agent to generate a sarcastic response to a given text using a DuckDuckGo search tool.\n", + "Here is a Task to crawl a website using the Spider Integration tool.\n", "\n", "More on how to define a task can be found [here](https://github.com/julep-ai/julep/blob/dev/docs/julep-concepts.md)." ] @@ -207,7 +207,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creating/Updating a task to generate a sarcastic response to a given text using a Intergation." + "Creating/Updating a task" ] }, { @@ -237,7 +237,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creates a execution worflow for the Task defined in the yaml file." + "Creates a execution worklow for the Task defined in the yaml file." ] }, { diff --git a/cookbooks/01-Website_Crawler_using_Spider.py b/cookbooks/01-Website_Crawler_using_Spider.py index 978fd3286..69d5785be 100644 --- a/cookbooks/01-Website_Crawler_using_Spider.py +++ b/cookbooks/01-Website_Crawler_using_Spider.py @@ -57,15 +57,20 @@ input={} ) +# Waiting for the execution to complete +import time +time.sleep(5) + # Getting the execution details execution = client.executions.get(execution.id) print("Execution output:", execution.output) # Listing all the steps of a defined task transitions = client.executions.transitions.list(execution_id=execution.id).items -print("Execution transitions:", transitions) +print("Execution Steps:") +for transition in transitions: + print(transition) -# Streaming the execution steps +# Stream the steps of the defined task print("Streaming execution transitions:") -for transition in client.executions.transitions.stream(execution_id=execution.id): - print(transition) \ No newline at end of file +print(client.executions.transitions.stream(execution_id=execution.id)) \ No newline at end of file diff --git a/cookbooks/02-Sarcastic_News_Headline_Generator.ipynb b/cookbooks/02-Sarcastic_News_Headline_Generator.ipynb index fded810a1..fb7957a5f 100644 --- a/cookbooks/02-Sarcastic_News_Headline_Generator.ipynb +++ b/cookbooks/02-Sarcastic_News_Headline_Generator.ipynb @@ -200,8 +200,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here is a Task which uses a agent to generate a sarcastic response to a given text using a DuckDuckGo search tool.\n", - "\n", "More on how to define a task can be found [here](https://github.com/julep-ai/julep/blob/dev/docs/julep-concepts.md)." ] }, @@ -242,7 +240,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creating/Updating a task to generate a sarcastic response to a given text using a Intergation." + "Creating/Updating a task." ] }, { @@ -272,7 +270,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creates a execution worflow for the Task defined in the yaml file." + "Creates a execution worklow for the Task defined in the yaml file." ] }, { diff --git a/cookbooks/02-Sarcastic_News_Headline_Generator.py b/cookbooks/02-Sarcastic_News_Headline_Generator.py index c658a6569..cf305c15e 100644 --- a/cookbooks/02-Sarcastic_News_Headline_Generator.py +++ b/cookbooks/02-Sarcastic_News_Headline_Generator.py @@ -75,15 +75,21 @@ } ) +# Waiting for the execution to complete +import time +time.sleep(5) + # Getting the execution details execution = client.executions.get(execution.id) print("Execution output:", execution.output) # Listing all the steps of a defined task transitions = client.executions.transitions.list(execution_id=execution.id).items -print("Execution transitions:", transitions) +print("Execution Steps:") +for transition in transitions: + print(transition) # Stream the steps of the defined task print("Streaming execution transitions:") -for transition in client.executions.transitions.stream(execution_id=execution.id): - print(transition) +print(client.executions.transitions.stream(execution_id=execution.id)) + diff --git a/cookbooks/03-SmartResearcher_With_WebSearch.ipynb b/cookbooks/03-SmartResearcher_With_WebSearch.ipynb index c01b54652..9bb6436c4 100644 --- a/cookbooks/03-SmartResearcher_With_WebSearch.ipynb +++ b/cookbooks/03-SmartResearcher_With_WebSearch.ipynb @@ -213,8 +213,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here is a Task which uses a agent to generate a sarcastic response to a given text using a DuckDuckGo and Wikipedia tool.\n", - "\n", "More on how to define a task can be found [here](https://github.com/julep-ai/julep/blob/dev/docs/julep-concepts.md)." ] }, @@ -272,7 +270,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creating/Updating a task to generate a sarcastic response to a given text using a Intergation." + "Creating/Updating a task." ] }, { @@ -302,7 +300,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creates a execution worflow for the Task defined in the yaml file." + "Creates a execution worklow for the Task defined in the yaml file." ] }, { diff --git a/cookbooks/03-SmartResearcher_With_WebSearch.py b/cookbooks/03-SmartResearcher_With_WebSearch.py index 9996a5dd5..6b0b210fb 100644 --- a/cookbooks/03-SmartResearcher_With_WebSearch.py +++ b/cookbooks/03-SmartResearcher_With_WebSearch.py @@ -1,6 +1,6 @@ import uuid from julep import Client -import yaml +import yaml, time # Global UUID is generated for agent and task AGENT_UUID = uuid.uuid4() @@ -92,13 +92,19 @@ print(execution.id) +# Wait for the execution to complete +time.sleep(10) + # Getting the execution details execution = client.executions.get(execution.id) print(execution.output) # Listing all the steps of a defined task transitions = client.executions.transitions.list(execution_id=execution.id).items -print(transitions) +print("Execution Steps:") +for transition in transitions: + print(transition) -# Streaming the execution steps -client.executions.transitions.stream(execution_id=execution.id) \ No newline at end of file +# Stream the steps of the defined task +print("Streaming execution transitions:") +print(client.executions.transitions.stream(execution_id=execution.id)) \ No newline at end of file diff --git a/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb b/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb index d7fc39840..dad492f20 100644 --- a/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb +++ b/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "div align=\"center\">\n", + "
\n", " \"julep\"\n", "
\n", "\n", @@ -219,8 +219,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here is a Task which uses a agent to generate a sarcastic response to a given text using a DuckDuckGo and Wikipedia tool.\n", - "\n", "More on how to define a task can be found [here](https://github.com/julep-ai/julep/blob/dev/docs/julep-concepts.md)." ] }, @@ -295,7 +293,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creating/Updating a task to generate a sarcastic response to a given text using a Intergation." + "Creating/Updating a task." ] }, { @@ -325,7 +323,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creates a execution worflow for the Task defined in the yaml file." + "Creates a execution worklow for the Task defined in the yaml file." ] }, { diff --git a/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.py b/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.py index cde5d71a6..bd446a849 100644 --- a/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.py +++ b/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.py @@ -107,6 +107,10 @@ print(f"Execution ID: {execution.id}") +# Wait for the execution to complete +import time +time.sleep(10) + # Getting the execution details execution = client.executions.get(execution.id) print("Execution Output:") @@ -114,10 +118,11 @@ # List all steps of the executed task print("Execution Steps:") -for item in client.executions.transitions.list(execution_id=execution.id).items: - print(item) +transitions = client.executions.transitions.list(execution_id=execution.id).items +print("Execution Steps:") +for transition in transitions: + print(transition) -# Stream the execution steps in real-time -print("Streaming Execution Steps:") -for step in client.executions.transitions.stream(execution_id=execution.id): - print(step) \ No newline at end of file +# Stream the steps of the defined task +print("Streaming execution transitions:") +print(client.executions.transitions.stream(execution_id=execution.id)) \ No newline at end of file diff --git a/cookbooks/05-Basic_Agent_Creation_and_Interaction.py b/cookbooks/05-Basic_Agent_Creation_and_Interaction.py index c701471f7..05b16f7bd 100644 --- a/cookbooks/05-Basic_Agent_Creation_and_Interaction.py +++ b/cookbooks/05-Basic_Agent_Creation_and_Interaction.py @@ -1,3 +1,5 @@ +# UNDER CONSTRUCTION - NOT WORKING YET + import uuid from julep import Client @@ -26,7 +28,7 @@ agent_id=AGENT_UUID, name=name, about=about, - model="gpt-4-turbo", + model="gpt-4o", ) print(f"Agent created with ID: {agent.id}") diff --git a/cookbooks/06-Designing_Multi-Step_Tasks.py b/cookbooks/06-Designing_Multi-Step_Tasks.py index 395f409cf..b623ae378 100644 --- a/cookbooks/06-Designing_Multi-Step_Tasks.py +++ b/cookbooks/06-Designing_Multi-Step_Tasks.py @@ -1,5 +1,5 @@ import uuid -import yaml +import yaml, time from julep import Client # Global UUID is generated for agent and task @@ -28,14 +28,13 @@ agent_id=AGENT_UUID, name=name, about=about, - model="gpt-4-turbo", + model="gpt-4o", ) # Add a web search tool to the agent client.agents.tools.create( agent_id=AGENT_UUID, name="web_search", - description="Search the web for information.", integration={ "provider": "brave", "method": "search", @@ -71,7 +70,7 @@ # Step 2: Tool Call - Web search for each question - foreach: - in: "_.split('\n')" + in: _.split('\\n') do: tool: web_search arguments: @@ -121,10 +120,13 @@ print(f"Execution ID: {execution.id}") +# Wait for the execution to complete +time.sleep(10) + # Getting the execution details execution = client.executions.get(execution.id) print("Execution Output:") -print(execution.output) +print(client.executions.transitions.list(execution_id=execution.id).items[0].output) # Listing all the steps of a defined task transitions = client.executions.transitions.list(execution_id=execution.id).items @@ -132,7 +134,6 @@ for transition in transitions: print(transition) -# Streaming the execution steps -print("Streaming Execution Steps:") -for transition in client.executions.transitions.stream(execution_id=execution.id): - print(transition) \ No newline at end of file +# Stream the steps of the defined task +print("Streaming execution transitions:") +print(client.executions.transitions.stream(execution_id=execution.id)) \ No newline at end of file diff --git a/cookbooks/07-Integrating_External_Tools_and_APIs.py b/cookbooks/07-Integrating_External_Tools_and_APIs.py index fa93f687a..71d8780d3 100644 --- a/cookbooks/07-Integrating_External_Tools_and_APIs.py +++ b/cookbooks/07-Integrating_External_Tools_and_APIs.py @@ -19,10 +19,10 @@ agent_id=AGENT_UUID, name=name, about=about, - model="gpt-4-turbo", + model="gpt-4o", ) -# Defining a Task +# Defining a Task with various step types task_def = yaml.safe_load(""" name: Comprehensive Analysis Report @@ -42,14 +42,14 @@ integration: provider: brave setup: - api_key: "YOUR_BRAVE_API_KEY" + api_key: "YOUR_API_KEY" - name: weather type: integration integration: provider: weather setup: - openweathermap_api_key: "YOUR_OPENWEATHERMAP_API_KEY" + openweathermap_api_key: "YOUR_API_KEY" - name: wikipedia type: integration @@ -59,20 +59,20 @@ main: - tool: brave_search arguments: - query: "{{inputs[0].topic}} latest developments" + query: "inputs[0].topic + ' latest developments'" - tool: weather arguments: - location: "{{inputs[0].location}}" + location: inputs[0].location - tool: wikipedia arguments: - query: "{{inputs[0].topic}}" + query: inputs[0].topic - prompt: - role: system content: >- - You are a comprehensive analyst. Your task is to create a detailed report on the topic "{{inputs[0].topic}}" + You are a comprehensive analyst. Your task is to create a detailed report on the topic {{inputs[0].topic}} using the information gathered from various sources. Include the following sections in your report: 1. Overview (based on Wikipedia data) @@ -88,8 +88,6 @@ Provide a well-structured, informative report that synthesizes information from all these sources. unwrap: true - -- return: _ """) # Creating/Updating a task @@ -104,23 +102,27 @@ task_id=task.id, input={ "topic": "Renewable Energy", - "location": "Berlin, Germany" + "location": "Berlin" } ) print(f"Execution ID: {execution.id}") +# Waiting for the execution to complete +import time +time.sleep(5) + # Getting the execution details execution = client.executions.get(execution.id) print("Execution Output:") print(execution.output) # List all steps of the executed task +transitions = client.executions.transitions.list(execution_id=execution.id).items print("Execution Steps:") -for item in client.executions.transitions.list(execution_id=execution.id).items: - print(item) +for transition in transitions: + print(transition) -# Stream the execution steps in real-time -print("Streaming Execution Steps:") -for step in client.executions.transitions.stream(execution_id=execution.id): - print(step) \ No newline at end of file +# Stream the steps of the defined task +print("Streaming execution transitions:") +print(client.executions.transitions.stream(execution_id=execution.id)) \ No newline at end of file diff --git a/cookbooks/08-Managing_Persistent_Sessions.py b/cookbooks/08-Managing_Persistent_Sessions.py index 40077b7df..ab2472ac8 100644 --- a/cookbooks/08-Managing_Persistent_Sessions.py +++ b/cookbooks/08-Managing_Persistent_Sessions.py @@ -27,7 +27,7 @@ agent_id=AGENT_UUID, name="Session Manager", about="An AI agent specialized in managing persistent sessions and context.", - model="gpt-4-turbo", + model="gpt-4o", ) # Defining a task for managing user context @@ -44,7 +44,7 @@ main: - prompt: - role: system + - role: system content: >- You are a session management agent. Your task is to maintain context across user interactions. Here's the current context: {{inputs[0].session_context}} @@ -53,16 +53,17 @@ Respond to the user and update the context with any new relevant information. unwrap: true - + - evaluate: - updated_context: >- - {**inputs[0].session_context, + session_context: >- + { + **inputs[0].session_context, 'last_interaction': inputs[0].user_input, - 'agent_response': _} + 'agent_response': _} - return: response: _ - context: outputs[1].updated_context + context: outputs[1].session_context """) # Creating the task @@ -78,7 +79,7 @@ def user_interaction(prompt): # Create a session session = client.sessions.create( - agent_id=AGENT_UUID, + agent=agent.id, context_overflow="adaptive" # Use adaptive context management ) @@ -100,10 +101,16 @@ def user_interaction(prompt): # Get the execution result result = client.executions.get(execution.id) + + # Wait for the execution to complete + time.sleep(2) # Update the context and print the response - context = result.output['context'] - print(f"Agent: {result.output['response']}") + final_response = client.executions.transitions.list(execution_id=result.id).items[0].output + print(final_response) + # print(client.executions.transitions.list(execution_id=result.id).items[0]) + context = final_response['session_context'] + print(f"Agent: {final_response['session_context']['agent_response']}") print(f"Updated Context: {context}") print() @@ -127,11 +134,13 @@ def user_interaction(prompt): ) overflow_result = client.executions.get(overflow_execution.id) -print(f"Agent response to large input: {overflow_result.output['response']}") -print(f"Updated context after overflow: {overflow_result.output['context']}") +# Wait for the execution to complete +time.sleep(2) +overflow_response = client.executions.transitions.list(execution_id=overflow_result.id).items[0].output +print(f"Agent response to large input: {overflow_response['session_context']['agent_response']}") +print(f"Updated context after overflow: {overflow_response['session_context']}") # Display session history print("\nSession History:") -history = client.sessions.messages.list(session_id=session.id) -for message in history.items: - print(f"{message.role}: {message.content}") \ No newline at end of file +history = client.sessions.history(session_id=session.id) +print(history) diff --git a/cookbooks/09-User_Management_and_Personalization.py b/cookbooks/09-User_Management_and_Personalization.py index 18f9df238..50ada9570 100644 --- a/cookbooks/09-User_Management_and_Personalization.py +++ b/cookbooks/09-User_Management_and_Personalization.py @@ -12,7 +12,7 @@ # 9. Display updated personalized recommendations after preference changes import uuid -import yaml +import yaml, time from julep import Client # Global UUIDs for agent and tasks @@ -29,7 +29,7 @@ agent_id=AGENT_UUID, name="Personalization Assistant", about="An AI agent specialized in user management and personalized content recommendations.", - model="gpt-4-turbo", + model="gpt-4o", ) # Defining a task for user registration and profile creation @@ -48,7 +48,7 @@ main: - prompt: - role: system + - role: system content: >- You are a user registration assistant. Create a user profile based on the following information: Username: {{inputs[0].username}} @@ -58,15 +58,13 @@ unwrap: true - evaluate: - user_profile: >- - { - "username": inputs[0].username, - "interests": inputs[0].interests, - "bio": _.split('\n\n')[0], - "content_preferences": _.split('\n\n')[1] - } - -- return: outputs[1].user_profile + username: inputs[0].username + interests: inputs[0].interests + bio: _.split('\\n')[0] + content_preferences: _.split('\\n')[1] + +- return: + profile: _ """) # Creating the registration task @@ -85,44 +83,38 @@ properties: user_profile: type: object - -tools: -- name: content_database - type: integration - integration: - provider: mock - setup: - data: [ - {"id": 1, "title": "Introduction to AI", "category": "Technology"}, - {"id": 2, "title": "Healthy Eating Habits", "category": "Health"}, - {"id": 3, "title": "Financial Planning 101", "category": "Finance"}, - {"id": 4, "title": "The Art of Photography", "category": "Art"}, - {"id": 5, "title": "Beginner's Guide to Yoga", "category": "Fitness"} - ] + description: User's profile containing their interests and preferences. + content_list: + type: array + description: List of available content to recommend from. + items: + type: object + properties: + id: + type: integer + title: + type: string + category: + type: string main: -- tool: content_database - arguments: {} - - prompt: - role: system + - role: system content: >- You are a content recommendation system. Based on the user's profile and the available content, recommend 3 pieces of content that best match the user's interests and preferences. - + User Profile: {{inputs[0].user_profile}} - + Available Content: - {{outputs[0]}} - + {{inputs[0].content_list}} + Provide your recommendations in the following format: 1. [Content ID] - [Content Title] - Reason for recommendation 2. [Content ID] - [Content Title] - Reason for recommendation 3. [Content ID] - [Content Title] - Reason for recommendation unwrap: true - -- return: _ """) # Creating the recommendation task @@ -141,19 +133,35 @@ def register_user(username, interests): "interests": interests } ) + # Wait for the execution to complete + time.sleep(2) result = client.executions.get(execution.id) - return result.output + user_result = client.executions.transitions.list(execution_id=result.id).items[0].output + return user_result -# Function to get personalized content recommendations +# Function to get personalized recommendations for a user def get_recommendations(user_profile): + content_list = [ + {"id": 1, "title": "Introduction to AI", "category": "Technology"}, + {"id": 2, "title": "Healthy Eating Habits", "category": "Health"}, + {"id": 3, "title": "Financial Planning 101", "category": "Finance"}, + {"id": 4, "title": "The Art of Photography", "category": "Art"}, + {"id": 5, "title": "Beginner's Guide to Yoga", "category": "Fitness"} + ] + execution = client.executions.create( task_id=RECOMMENDATION_TASK_UUID, input={ - "user_profile": user_profile + "user_profile": user_profile, + "content_list": content_list } ) + # Wait for the execution to complete + time.sleep(2) result = client.executions.get(execution.id) - return result.output + recommendation_respose = client.executions.transitions.list(execution_id=result.id).items[0].output + return recommendation_respose + # Function to update user preferences def update_user_preferences(user_profile, new_interests): diff --git a/cookbooks/10-Document_Management_and_Search.py b/cookbooks/10-Document_Management_and_Search.py index 87cc492aa..70db82d3f 100644 --- a/cookbooks/10-Document_Management_and_Search.py +++ b/cookbooks/10-Document_Management_and_Search.py @@ -10,8 +10,10 @@ # 7. Execute the document search task # 8. Display the search results +# UNDER CONSTRUCTION - YAML is working but the flow is not correct yet + import uuid -import yaml +import yaml,time from julep import Client # Global UUID is generated for agent and tasks @@ -43,21 +45,32 @@ items: type: object properties: + tile: + type: string content: type: string metadata: type: object + +tools: +- name: document_create + system: + resource: agent + subresource: doc + operation: create main: - over: inputs[0].documents map: tool: document_upload arguments: + agent_id: "'{agent.id}'" + title: _.title content: _.content metadata: _.metadata - prompt: - role: system + - role: system content: >- You have successfully uploaded and indexed {{len(outputs[0])}} documents. Provide a summary of the uploaded documents. @@ -82,14 +95,22 @@ filters: type: object +tools: +- name: document_search + system: + resource: agent + subresource: doc + operation: search + main: - tool: document_search arguments: - query: inputs[0].query - filters: inputs[0].filters + agent_id: "'{agent.id}'" + text: inputs[0].query + metadata_filters: inputs[0].filters - prompt: - role: system + - role: system content: >- Based on the search results, provide a summary of the most relevant documents found. Search query: {{inputs[0].query}} @@ -97,6 +118,7 @@ Results: {{outputs[0]}} + unwrap: true """) # Creating the search task @@ -109,14 +131,17 @@ # Sample documents sample_documents = [ { + "Title": "The Impact of Technology on Society", "content": "Artificial Intelligence (AI) is revolutionizing various industries, including healthcare, finance, and transportation.", "metadata": {"category": "technology", "author": "John Doe"} }, { + "Title": "Climate Change and Global Warming", "content": "Climate change is a pressing global issue that requires immediate action from governments, businesses, and individuals.", "metadata": {"category": "environment", "author": "Jane Smith"} }, { + "Title": "Remote Work and Digital Transformation", "content": "The COVID-19 pandemic has accelerated the adoption of remote work and digital technologies across many organizations.", "metadata": {"category": "business", "author": "Alice Johnson"} } @@ -129,8 +154,12 @@ ) print("Uploading and indexing documents...") +# Wait for the execution to complete +time.sleep(5) upload_result = client.executions.get(upload_execution.id) -print(upload_result.output) +upload_response = client.executions.transitions.list(upload_execution.id).items[0].output +print("Upload Result:") +print(upload_response) # Execute the document search task search_execution = client.executions.create( @@ -142,9 +171,9 @@ ) print("\nSearching documents...") +# Wait for the execution to complete +time.sleep(5) search_result = client.executions.get(search_execution.id) -print(search_result.output) - # Display the search results print("\nSearch Results:") for transition in client.executions.transitions.list(execution_id=search_execution.id).items: @@ -153,4 +182,5 @@ print(f"- {doc['content']} (Score: {doc['score']})") print("\nSearch Summary:") -print(search_result.output) \ No newline at end of file +search_response = client.executions.transitions.list(search_result.id).items[0].output +print(search_response) \ No newline at end of file diff --git a/cookbooks/11-Advanced_Chat_Interactions.py b/cookbooks/11-Advanced_Chat_Interactions.py index 692112be1..1a6b13026 100644 --- a/cookbooks/11-Advanced_Chat_Interactions.py +++ b/cookbooks/11-Advanced_Chat_Interactions.py @@ -12,6 +12,8 @@ # d. Integrating external information during the conversation # 6. Display the chat history and any relevant metrics +# UNDER CONSTRUCTION - YAML is working but the flow is not correct yet + import uuid import yaml import os @@ -34,14 +36,13 @@ agent_id=AGENT_UUID, name="Advanced Chat Assistant", about="An AI agent capable of handling complex conversations with context management and external integrations.", - model="gpt-4-turbo", + model="gpt-4o", ) # Add a web search tool to the agent client.agents.tools.create( agent_id=AGENT_UUID, name="web_search", - description="Search the web for information.", integration={ "provider": "brave", "method": "search", @@ -74,48 +75,49 @@ integration: provider: weather setup: - api_key: "YOUR_WEATHER_API_KEY" + api_key: "API_KEY" main: - evaluate: context_length: len(inputs[0].chat_history) -- if: - condition: _.context_length > 10 - then: - - evaluate: - summarized_history: "Summarize the following chat history: " + str(inputs[0].chat_history[-10:]) - - prompt: - role: system - content: >- - You are an advanced chat assistant. Here's a summary of the recent conversation: - {{outputs[1].summarized_history}} - - Now, respond to the user's latest input: {{inputs[0].user_input}} - else: - - prompt: - role: system - content: >- - You are an advanced chat assistant. Here's the conversation history: - {{inputs[0].chat_history}} - - Now, respond to the user's latest input: {{inputs[0].user_input}} - -- if: - condition: "weather" in inputs[0].user_input.lower() - then: - - tool: weather_api - arguments: - location: "New York" - - prompt: - role: system - content: >- - The user mentioned weather. Here's the current weather information for New York: - {{outputs[3]}} - - Incorporate this information into your response. - -- return: _ +- if: "_.context_length > '10'" + then: + evaluate: + summarized_history: str(inputs[0].chat_history[-10:]) + prompt: + - role: system + content: >- + You are an advanced chat assistant. Here's a summary of the recent conversation: + {{outputs[1].summarized_history}} + + Now, respond to the user's latest input: {{inputs[0].user_input}} + unwrap: true + else: + prompt: + - role: system + content: >- + You are an advanced chat assistant. Here's the conversation history: + {{inputs[0].chat_history}} + + Now, respond to the user's latest input: {{inputs[0].user_input}} + unwrap: true + +- if: "'weather' in inputs[0].user_input.lower()" + then: + tool: weather_api + arguments: + location: inputs[0].user_input.lower.split('weather')[1].strip() + prompt: + - role: system + content: >- + The user mentioned weather. Here's the current weather information for {{inputs[0].user_input.lower.split('weather')[1].strip()}} + + Incorporate this information into your response. + unwrap: true + +- return: + summary: _ """) # Creating the chat task @@ -139,7 +141,7 @@ def run_chat_session(): chat_history = [] print("Starting advanced chat session. Type 'exit' to end the conversation.") - session = client.sessions.create(agent_id=AGENT_UUID) + session = client.sessions.create(agent=AGENT_UUID) while True: user_input = get_user_input() @@ -155,8 +157,11 @@ def run_chat_session(): "chat_history": chat_history } ) - + # Wait for the execution to complete + time.sleep(3) result = client.executions.get(execution.id) + print(client.executions.transitions.list(execution.id).items) + print(f"Execution result: {result.output}") assistant_response = result.output chat_history.append({"role": "assistant", "content": assistant_response}) @@ -169,9 +174,4 @@ def run_chat_session(): display_chat_history(chat_history) # Run the chat session -run_chat_session() - -# Display execution metrics (optional) -print("\nExecution Metrics:") -for transition in client.executions.transitions.list(execution_id=execution.id).items: - print(f"Step: {transition.type}, Duration: {transition.duration_ms}ms") \ No newline at end of file +run_chat_session() \ No newline at end of file diff --git a/cookbooks/12-Monitoring_Task_Executions.py b/cookbooks/12-Monitoring_Task_Executions.py index 7e5f576a0..675ff27a1 100644 --- a/cookbooks/12-Monitoring_Task_Executions.py +++ b/cookbooks/12-Monitoring_Task_Executions.py @@ -12,6 +12,8 @@ # 5. Execute the task and demonstrate real-time monitoring # 6. Display execution summary and metrics +# UNDER CONSTRUCTION - NOT WORKING YET + import uuid import yaml from julep import Client @@ -30,7 +32,7 @@ agent_id=AGENT_UUID, name="Task Execution Monitor", about="An AI agent designed to monitor and manage complex task executions.", - model="gpt-4-turbo", + model="gpt-4o", ) # Defining a multi-step task that simulates a complex workflow diff --git a/cookbooks/13-Error_Handling_and_Recovery.py b/cookbooks/13-Error_Handling_and_Recovery.py index b45732ed5..f0cdf68be 100644 --- a/cookbooks/13-Error_Handling_and_Recovery.py +++ b/cookbooks/13-Error_Handling_and_Recovery.py @@ -9,6 +9,8 @@ # 6. Show how to log and report errors # 7. Demonstrate graceful degradation when a step fails +# UNDER CONSTRUCTION - NOT WORKING YET + import uuid import yaml import time @@ -27,7 +29,7 @@ agent_id=AGENT_UUID, name="Error Handler", about="An AI agent specialized in demonstrating error handling and recovery mechanisms.", - model="gpt-4-turbo", + model="gpt-4o", ) # Defining a task with potential errors and recovery mechanisms @@ -73,45 +75,45 @@ type: string main: + - switch: - value: inputs[0].operation - cases: - divide: - - tool: divide - arguments: - divisor: inputs[0].value - on_error: - retry: - max_attempts: 3 - delay: 2 - fallback: - return: "Error: Division by zero or invalid input" - api_call: - - tool: api_call - arguments: - endpoint: "/status/{{inputs[0].value}}" - on_error: - retry: - max_attempts: 3 - delay: 5 - fallback: - return: "Error: API call failed after multiple attempts" - process_data: - - evaluate: - data: "'Sample data: ' + str(inputs[0].value)" - - tool: process_data - arguments: - data: _.data - on_error: - log: "Error occurred while processing data" - return: "Error: Data processing failed" + case: "inputs[0].operation == 'divide'" + tool: divide + arguments: + divisor: inputs[0].value + on_error: + retry: + max_attempts: 3 + delay: 2 + fallback: + return: "Error: Division by zero or invalid input" + case: "inputs[0].operation == 'api_call'" + tool: api_call + arguments: + endpoint: "/status/{{inputs[0].value}}" + on_error: + retry: + max_attempts: 3 + delay: 5 + fallback: + return: "Error: API call failed after multiple attempts" + case: "inputs[0].operation == 'process_data'" + evaluate: + data: "'Sample data: ' + str(inputs[0].value)" + tool: process_data + arguments: + data: _.data + on_error: + log: "Error occurred while processing data" + return: "Error: Data processing failed" - prompt: - role: system + - role: system content: >- Summarize the result of the operation: Operation: {{inputs[0].operation}} - Result: {{_}} + Result: {{_}}] + unwrap: true """) # Creating the task diff --git a/docs/README.md b/docs/README.md index d57877707..5359f6c38 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,7 +38,7 @@ We're excited to welcome new contributors to the Julep project! We've created several "good first issues" to help you get started. Here's how you can contribute: -1. Check out our [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute. +1. Check out our [CONTRIBUTING.md](https://github.com/julep-ai/julep/blob/dev/CONTRIBUTING.md) file for guidelines on how to contribute. 2. Browse our [good first issues](https://github.com/julep-ai/julep/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to find a task that interests you. 3. If you have any questions or need help, don't hesitate to reach out on our [Discord](https://discord.com/invite/JTSBGRZrzj) channel. @@ -69,6 +69,8 @@ Exciting news! We're participating in DevFest.AI throughout October 2024! 🗓

📖 Table of Contents

+- [🌟 Call for Contributors!](#-call-for-contributors) + - [🎉 DevFest.AI October 2024](#-devfestai-october-2024) - [Introduction](#introduction) - [Quick Example](#quick-example) - [Key Features](#key-features) @@ -92,6 +94,11 @@ Exciting news! We're participating in DevFest.AI throughout October 2024! 🗓 - [Concepts](#concepts) - [Understanding Tasks](#understanding-tasks) - [Types of Workflow Steps](#types-of-workflow-steps) + - [Common Steps](#common-steps) + - [Key-Value Steps](#key-value-steps) + - [Iteration Steps](#iteration-steps) + - [Conditional Steps](#conditional-steps) + - [Other Control Flow](#other-control-flow) - [Advanced Features](#advanced-features) - [Adding Tools to Agents](#adding-tools-to-agents) - [Managing Sessions and Users](#managing-sessions-and-users)