diff --git a/__init__.py b/__init__.py index 3727c67..7c613da 100644 --- a/__init__.py +++ b/__init__.py @@ -4,4 +4,4 @@ JIVAS is an Agentic Framework for rapidly prototyping and deploying graph-based, AI solutions. """ -__version__ = "2.1.22" +__version__ = "2.1.23" diff --git a/core/jivas/agent/action/action.jac b/core/jivas/agent/action/action.jac index 3b2e99b..8c9445c 100644 --- a/core/jivas/agent/action/action.jac +++ b/core/jivas/agent/action/action.jac @@ -4,6 +4,8 @@ import from typing { Union } import from logging { Logger } import from jivas.agent.core.graph_node { GraphNode } import from jivas.agent.memory.collection { Collection } +import from jivas.agent.memory.frame { Frame } +import from jivas.agent.memory.interaction { Interaction } node Action(GraphNode) { # represents an execution on the agent action graph @@ -28,6 +30,8 @@ node Action(GraphNode) { # override to execute operations upon registration of action def on_register() { } + def run(frame_node:Frame, interaction_node:Interaction){} + # override to execute operations upon the reload of action def on_reload() { } diff --git a/core/jivas/agent/action/subgraph_action/completed_state.jac b/core/jivas/agent/action/subgraph_action/completed_state.jac new file mode 100644 index 0000000..7d0ddbc --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/completed_state.jac @@ -0,0 +1,193 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.memory.frame { Frame } + + + +node CompletedState(GraphNode) { + # Represents an execution on a subgraph on the agent action graph + has collection_id: str = ""; + has auto_confirm: bool = False; + has model_action: str = "LangChainModelAction"; + has model_name: str = "gpt-4o"; + has model_max_tokens:int = 4096; + has model_temperature: float = 0.3; + has history: bool = True; + has history_size: int = 3; + has max_statement_length: int = 2048; + has label: str = ""; + has state_info:dict = {}; + has enabled: bool = True; + + has directive: str = """ + Perform the following steps to confirm user submission: + a. Summarize submission: + - Extract all user-provided submission details from: + {summary} + - Format them as a clear, bulleted list under a statement that indicates that it is a list of the information gathered. + - Do not put internal settings such as "revised" or "confirm response" in the displayed list + - Do not put any items on the list with a value of N/A + b. Request Explicit Confirmation: + Present the key and summary followed by: + - a request to know if the presented details are accurate + - a message to inform the user that they can request changes or cancel altogether. + """; + + has prompt:str = """ + Analyze **ONLY the latest user message** the conversation history above. Detect ONLY explicit signals for confirmation (yes/affirmative) + Follow these rules: + + # Confirmation Detection + Set "confirm_response" to true for: "yes", "sure", "confirmed", "yeah", "yep", "absolutely", "okay" + clear context. + Set "confirm_response" to false ONLY for explicit negative or revision signals, including: + - Direct negatives: "no" + - Revision or correction requests: "I'd like to make an adjustment", "need to make a change", "revision", "change my answer", "not correct", "incorrect", "needs update" + - Suggestions to edit or change: phrases like "actually, please change...", "no, can you change...", "can you update...", "please edit...", "I'd like to change...", "can you correct...", "let's fix...", "could you modify...", or similar expressions indicating a desire to alter or correct previous information. + + + Return ONLY a JSON structure with a single detected key (confirm_response) set to true, + or "confirm_response" set to false otherwise. No commentary. + If nothing is detected, return an empty JSON object. No delimiters! + """; + + # override to execute operations upon enabling of action + def on_enable() { } + + # override to execute operations upon disabling of action + def on_disable() { } + + def touch(frame:Frame) -> bool { + return True; + } + + def run(frame:Frame) -> Union[str, bool] { + frame_node = frame.frame_node; + agent_node = frame.agent_node; + + states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + revised = states_data.get("revised", False); + + if self.auto_confirm { + branch_choice_response ={"confirm_response": True}; + self.update_responses(branch_choice_response, frame); + } + elif not revised{ + # Check for confirmation in the latest user message + branch_choice_response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); + self.update_responses(branch_choice_response, frame); + } + elif revised{ + branch_choice_response = {}; + } + + confirmed = branch_choice_response.get('confirm_response', None); + + + # If there is no confirmation or abortion + if not branch_choice_response { + # Retrieve the interview session from the frame node + responses = frame_node.data_get(key=f"{frame.action_label}_results"); + + if responses and isinstance(responses, dict) and len(responses) > 0 { + summary_lines = []; + for (field, value) in responses.items() { + summary_lines.append(f"- **{field}**: {value}"); + } + summary = "\n".join(summary_lines); + } else { + summary = ""; + } + + directive = self.directive.replace("{summary}", summary); + + # Change revised in frame to false + revised_state = {"revised":False}; + self.update_responses(revised_state, frame); + return directive; + } + elif confirmed{ + return True; + } + elif not confirmed{ + return False; + } + } + + def call_llm(prompt:str, history:Union[bool,None] = None, json_only:bool = False, frame_node:Frame, agent_node:GraphNode) -> Union[str, dict, None] { + # performs function tool calling for extracting question responses based on question + prompt_messages = []; + + if not prompt { + return None; + } + + use_history = self.history; + if history is not None { + use_history = history; + } + visitor_utterance = frame_node.data_get(key="visitor_utterance"); + + if not visitor_utterance { + return None; + } + + prompt_messages = [ + {"human":visitor_utterance}, + {"system":prompt} + ]; + + # prepare the final prompt with history. + if (use_history) { + statements = frame_node.get_transcript_statements(interactions = self.history_size, max_statement_length = self.max_statement_length, with_events = True); + + if (statements) { + # prepend statements to the prompt messages + prompt_messages = statements + prompt_messages; + } + + } + + model_action = agent_node.get_action(action_label=self.model_action); + + if model_action { + model_action_result = model_action.call_model( + prompt_messages=prompt_messages, + prompt_variables={}, + model_name=self.model_name, + model_temperature=self.model_temperature, + model_max_tokens=self.model_max_tokens + ); + + if model_action_result { + if json_only { + return model_action_result.get_json_result(); + } else { + return model_action_result.get_result(); + } + } + else { + return None; + } + } + } + + def update_responses(responses:dict, frame:Frame) { + print(f"New responses to merge: {responses}"); + stored_responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + + if type(stored_responses) is not dict or not stored_responses { + stored_responses = {}; + } + + # Merge new responses into stored_responses + for (key, value) in responses.items() { + stored_responses[key] = value; + } + + frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses); + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/confirmed_state.jac b/core/jivas/agent/action/subgraph_action/confirmed_state.jac new file mode 100644 index 0000000..2dd4a6f --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/confirmed_state.jac @@ -0,0 +1,38 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.memory.frame { Frame } + + +node ConfirmedState(State) { + # Represents an execution on a subgraph on the agent action graph + + has collection_id: str = ""; + has enabled: bool = True; + has state_info:dict = {}; + has directive: str = "Tell the user you have completed the process"; + + # override to execute operations upon enabling of action + def on_enable() { } + + # override to execute operations upon disabling of action + def on_disable() { } + + def touch(frame:Frame) -> bool { + # Always allow entering the COMPLETED state + return True; + } + + def run(frame:Frame) { + frame_node = frame.frame_node; + agent_node = frame.agent_node; + + # set any custom directive from daf + if ( directive := self.state_info.get("directive", "") ){ + self.directive = directive; + } + return self.directive; + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac new file mode 100644 index 0000000..ae4c95f --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -0,0 +1,62 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.memory.frame { Frame } + + +node InitialState(State) { + # Represents an execution on a subgraph on the agent action graph + + has collection_id: str = ""; + has label: str = ""; + has enabled: bool = True; + has state_info:dict = {}; + has abort:bool = False; + + has directive:str = "Tell the user that you will cancel the process"; + has prompt: str = """ + Analyze **ONLY the latest user message** the conversation history above. Detect ONLY explicit signals for + conversation termination (abort/stop), decline-to-answer (no answer/can't respond). + Follow these rules: + # Abort Detection + Set "abort_response" to true for: "stop", "cancel", "exit", "end chat", "nevermind", "abort", "terminate". + Do NOT include "abort_response" if not explicitly stated. + # Decline Detection + Set "decline_response" to true for: "no answer", "I don't know", "I have none", "no comment", "can't say", "nothing", "n/a", "decline to answer". + Do NOT include "decline_response" for partial answers, topic changes, or ambiguous non-responses. + + Return ONLY a JSON structure with a single detected key (abort_response, decline_response) set to true. + If nothing is detected, return an empty JSON object. No delimiters! + No commentary. Never guess - ambiguous cases = empty JSON. + + """; + + def touch(frame:Frame) -> bool { + # Always allow entering the Initial state + return True; + } + + def run(frame:Frame) { + frame_node = frame.frame_node; + agent_node = frame.agent_node; + + # check if abort was set to true + if self.abort{ + frame_node.data_set(key=f"{frame.action_label}_results", value={}); + self.abort = False; + return ""; + }else{ + response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); + if response.get("abort_response"){ + frame_node.data_set(key=f"{frame.action_label}_results", value={}); + return self.directive; + } + else{ + return True; + } + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/list_states.jac b/core/jivas/agent/action/subgraph_action/list_states.jac new file mode 100644 index 0000000..26072d4 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/list_states.jac @@ -0,0 +1,51 @@ +import logging; +import from logging { Logger } +import from jivas.agent.core.agent { Agent } +import from jivas.agent.action.action { Action } +import from jivas.agent.action.actions { Actions } +import from jivas.agent.action.subgraph_action.subgraph_action { SubgraphAction } +import from jivas.agent.action.agent_graph_walker { agent_graph_walker } + + +walker list_states(agent_graph_walker) { + # action endpoint for listing all documents processed by the deepdoc service + + has page:int = 1; + has per_page:int = 10; + has all:bool = True; # new flag to indicate whether to return all documents + has response:list[dict] = []; + has reporting:bool = True; + has agent_id:str = ""; + has label:str = ""; + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + class __specs__ { + static has private: bool = False; + static has excluded: list[str] = ["response"]; # exclude response from the specs + } + + can on_agent with Agent entry { + visit [-->](`?Actions); + } + + can on_actions with Actions entry { + visit [-->](`?SubgraphAction)(?enabled==True)(?label==self.label); + } + + + can on_action with Action entry { + # get the list of documents from the manifest + + if self.all { + self.response = here.list_states(page=self.page, limit=100); # fetch all documents + } else { + self.response = here.list_states(page=self.page, limit=self.per_page); # fetch paged documents + } + + if self.reporting { + report self.response; + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/revision_state.jac b/core/jivas/agent/action/subgraph_action/revision_state.jac new file mode 100644 index 0000000..dc011ba --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/revision_state.jac @@ -0,0 +1,200 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.memory.frame { Frame } + + + +node RevisionState(GraphNode) { + # Represents an execution on a subgraph on the agent action graph + has collection_id: str = ""; + has label: str = ""; + has state_info:dict = {}; + has states:list = []; + has model_action: str = "LangChainModelAction"; + has model_name: str = "gpt-4o"; + has model_max_tokens:int = 4096; + has model_temperature: float = 0.3; + has history: bool = True; + has history_size: int = 3; + has max_statement_length: int = 2048; + has directive: str = "Encourage the user to suggest any changes to the information provided."; + has enabled: bool = True; + + has prompt:str = """ + Review the user's message and the recorded responses to accurately extract the following entities and update the relevant recorded responses. + Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. + Include only keys for which you could extract a valid revised value adhering to all constraints. + Do not extract value if it is the same as the one in responses but always extract if the user states a different value from the one in the recorded responses. + + Entities to extract: + {entities} + + Recorded responses: + {responses} + + Return ONLY the JSON object with the revised entities, no delimiters. Do not include any other text or explanation. + Return an empty JSON if no changes were made. + The JSON must have the following structure (only include keys with valid values): + {sample_json} + """; + + # override to execute operations upon enabling of action + def on_enable() { } + + # override to execute operations upon disabling of action + def on_disable() { } + + def touch(frame:Frame) -> bool { + states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + state_value = states_data.get("confirm_response", ""); + if state_value{ + return False; + } + else{ + return True; + } + } + + def run(frame:Frame) -> Union[str, bool] { + frame_node = frame.frame_node; + agent_node = frame.agent_node; + + # Check for any changes in the latest user message + prompt = self.generate_revision_extraction_prompt(frame); + extraction_result = self.call_llm(prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); + + # If there is no confirmation or abortion + if extraction_result{ + # store the result in the frame node + self.update_responses(extraction_result, frame); + + #clearing confirm response so that completed state can processs it + confirm_response = {"confirm_response":{}, "revised":True}; + self.update_responses(confirm_response, frame); + return True; + } + else{ + directive = self.directive; + return directive; + } + } + + def generate_revision_extraction_prompt(frame:Frame) -> str { + # prepares the revision extraction prompt + + entities_list = []; + sample_json_lines = []; + + for state_info in self.states { + constraints = state_info.get('constraints', {}); + + if not constraints { + continue; + } + + desc = constraints.get('description', ''); + other_constraints = {k: v for (k, v) in constraints.items() if k != 'description'}; + constraint_strs = [f"{k}: {v}" for (k, v) in other_constraints.items()]; + constraint_part = f" ({', '.join(constraint_strs)})" if constraint_strs else ""; + entities_list.append(f"- {state_info.get("name")}: {desc}{constraint_part}"); + sample_json_lines.append(f" '{state_info.get("name")}': ''"); + } + + responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + # Convert the responses dict to a markdown bulleted list + if responses and isinstance(responses, dict) and len(responses) > 0 { + summary_lines = []; + for (field, value) in responses.items() { + summary_lines.append(f"- **{field}**: {value}"); + } + responses = "\n".join(summary_lines); + } + + entities = "\n".join(entities_list); + sample_json = '{\n' + ',\n'.join(sample_json_lines) + '\n}'; + # prepate the prompt + prompt = self.prompt.format(entities=entities, responses=responses, sample_json=sample_json); + # escape the conflicting symbols + prompt = prompt.replace('{', '{{').replace('}','}}'); + + return prompt; + } + + def call_llm(prompt:str, history:Union[bool,None] = None, json_only:bool = False, frame_node:Frame, agent_node:GraphNode) -> Union[str, dict, None] { + # performs function tool calling for extracting question responses based on question + prompt_messages = []; + + if not prompt { + return None; + } + + use_history = self.history; + if history is not None { + use_history = history; + } + visitor_utterance = frame_node.data_get(key="visitor_utterance"); + if not visitor_utterance { + return None; + } + + prompt_messages = [ + {"human":visitor_utterance}, + {"system":prompt} + ]; + + # prepare the final prompt with history. + if (use_history) { + statements = frame_node.get_transcript_statements(interactions = self.history_size, max_statement_length = self.max_statement_length, with_events = True); + + if (statements) { + # prepend statements to the prompt messages + prompt_messages = statements + prompt_messages; + } + + } + + model_action = agent_node.get_action(action_label=self.model_action); + + if model_action { + model_action_result = model_action.call_model( + prompt_messages=prompt_messages, + prompt_variables={}, + model_name=self.model_name, + model_temperature=self.model_temperature, + model_max_tokens=self.model_max_tokens + ); + + if model_action_result { + if json_only { + return model_action_result.get_json_result(); + } else { + return model_action_result.get_result(); + } + } + else { + return None; + } + } + + # return None; + } + + def update_responses(responses:dict, frame:Frame) { + stored_responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + + if type(stored_responses) is not dict or not stored_responses { + stored_responses = {}; + } + + # Merge new responses into stored_responses + for (key, value) in responses.items() { + stored_responses[key] = value; + } + + frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses); + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac new file mode 100644 index 0000000..e5b39d5 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -0,0 +1,232 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.memory.frame { Frame } +import from jivas.agent.action.action { Action } + +node State(GraphNode) { + # represents an execution on a subgraph on the agent action graph + + has collection_id:str = ""; + has label: str = ""; + has description: str = "specialized state"; + has enabled: bool = True; + has state_info: dict = {}; + + + #llm model parameters + has model_action: str = "LangChainModelAction"; + has model_name: str = "gpt-4o"; + has model_max_tokens:int = 4096; + has model_temperature: float = 0.3; + has history: bool = True; + has history_size: int = 3; + has max_statement_length: int = 2048; + + has extraction_prompt:str = """ + Review the user's message and the conversation history to accurately extract the following entities. + Only extract data that has not been specifically cancelled. + Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. + Include only keys for which you could extract a valid value adhering to all constraints. + + Entities to extract: + {entities} + + Return ONLY the JSON object with the extracted entities, no delimiters. Do not include any other text or explanation. + The JSON must have the following structure (only include keys with valid values): + {sample_json} + """; + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + + def postinit { + super.postinit(); + # list of node attributes which are protected from update operation + self.protected_attrs += ['_package', 'label', 'version', 'agent_id']; + # list of node attributes which should be excluded from export + self.transient_attrs += ['agent_id']; + } + + # override to execute operations upon enabling of action + def on_enable() { } + + # override to execute operations upon disabling of action + def on_disable() { } + + def touch(frame:Frame) -> bool { + frame_node = frame.frame_node; + + state_data = frame_node.data_get(key=f"{frame.action_label}_results"); + if state_data{ + state_value = state_data.get(self.label, None); + confirmed = state_data.get("confirm_response", False); + } + else{ + state_value = {}; + confirmed = False; + } + + if state_value or confirmed{ + return False; + } + + if (conditional := self.state_info.get('conditional', {})) { + + for (field, expected_value) in conditional.items(){ + actual_value = state_data.get(field, None); + if actual_value != expected_value{ + return False; + } + } + } + + # authorize the walker to run this action + return True; + + } + + def run(frame:Frame)->Union[str, bool]{ + agent_node = frame.agent_node; + frame_node = frame.frame_node; + required = self.state_info.get("required",""); + + prompt = self.generate_extraction_prompt(); + + extraction_result = self.call_llm(prompt=prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); + + if required is True and not extraction_result{ + question = self.state_info.get("question", ""); + constraints = self.state_info.get("constraints", {}); + description = constraints.get("description", ""); + + directive = "Tailor your response to get the information needed based on the following description: \n" + description + "\n Eg." + question; + + if(options:= constraints.get("options", "")){ + directive = directive + "\n They can choose from the list of options below\n" + str(options); + } + frame_node.data_set(key="directive", value=directive); + return directive; + } + elif required == False { + if not extraction_result{ + # set extraction result to N/A + extraction_result = {self.label:"N/A"}; + self.update_responses(extraction_result, frame); + } + } + if extraction_result{ + # store the result in the frame node + self.update_responses(extraction_result, frame); + return True; + } + } + + def generate_extraction_prompt() -> str { + # accepts the question index schema and prepares an extraction prompt + entities_list = []; + sample_json_lines = []; + + if(constraints := self.state_info.get('constraints', {})){ + + desc = constraints.get('description', ''); + other_constraints = {k: v for (k, v) in constraints.items() if k != 'description'}; + constraint_strs = [f"{k}: {v}" for (k, v) in other_constraints.items()]; + constraint_part = f" ({', '.join(constraint_strs)})" if constraint_strs else ""; + entities_list.append(f"- {self.state_info.get("name")}: {desc}{constraint_part}"); + sample_json_lines.append(f" '{self.state_info.get("name")}': ''"); + + + entities = "\n".join(entities_list); + sample_json = '{\n' + ',\n'.join(sample_json_lines) + '\n}'; + # prepate the prompt + prompt = self.extraction_prompt.format(entities=entities, sample_json=sample_json); + # escape the conflicting symbols + prompt = prompt.replace('{', '{{').replace('}','}}'); + + return prompt; + } + else { + return ""; + } + } + + def call_llm(prompt:str, history:Union[bool,None] = None, json_only:bool = False, frame_node:Frame, agent_node:GraphNode) -> Union[str, dict, None] { + # performs function tool calling for extracting question responses based on question + prompt_messages = []; + + if not prompt { + return None; + } + + use_history = self.history; + if history is not None { + use_history = history; + } + visitor_utterance = frame_node.data_get(key="visitor_utterance"); + if not visitor_utterance { + return None; + } + + prompt_messages = [ + {"human":visitor_utterance}, + {"system":prompt} + ]; + + # prepare the final prompt with history. + if (use_history) { + statements = frame_node.get_transcript_statements(interactions = self.history_size, max_statement_length = self.max_statement_length, with_events = True); + + if (statements) { + # prepend statements to the prompt messages + prompt_messages = statements + prompt_messages; + } + + } + + model_action = agent_node.get_action(action_label=self.model_action); + + if model_action { + model_action_result = model_action.call_model( + prompt_messages=prompt_messages, + prompt_variables={}, + model_name=self.model_name, + model_temperature=self.model_temperature, + model_max_tokens=self.model_max_tokens + ); + + if model_action_result { + if json_only { + return model_action_result.get_json_result(); + } else { + return model_action_result.get_result(); + } + } + else { + return None; + } + } + + # return None; + } + + def update_responses(responses:dict, frame:Frame) { + stored_responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + + if type(stored_responses) is not dict or not stored_responses { + stored_responses = {}; + } + + # Merge new responses into stored_responses + for (key, value) in responses.items() { + stored_responses[key] = value; + } + + frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses); + } + + +} diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac new file mode 100644 index 0000000..ff7fec4 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -0,0 +1,457 @@ +import logging; +import traceback; +import from enum { unique } +import from logging { Logger } +import from jivas.agent.action.action { Action } +import from jivas.agent.modules.data.node_get { node_get } +import from jivas.agent.modules.system.common { node_obj } +import from jac_cloud.core.archetype {BaseCollection, NodeAnchor} +import from jivas.agent.modules.data.node_pager { NodePager } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.action.subgraph_action.completed_state { CompletedState } +import from jivas.agent.action.subgraph_action.confirmed_state { ConfirmedState } +import from jivas.agent.action.subgraph_action.revision_state { RevisionState } +import from jivas.agent.action.subgraph_action.initial_state { InitialState } +import from jivas.agent.memory.collection { Collection } +import from jivas.agent.memory.frame { Frame } +import from jivas.agent.core.graph_walker { graph_walker } +import from jivas.agent.core.graph_node { GraphNode } + + +node SubgraphAction(Action) { + # represents an execution on a subgraph on the agent action graph + has persona_action:str = "PersonaInteractAction"; + has label: str = ""; + has description: str = "subgraph action that executes a series of states"; + has enabled: bool = True; + has states: list = []; + has initial_state:dict = {}; + has completed_state: dict = {}; + has confirmed_state:dict = {}; + has revision_state: dict = {}; + has parameters: list = []; + + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + def postinit { + super.postinit(); + # list of node attributes which are protected from update operation + self.protected_attrs += ['label', 'version', 'agent_id']; + # list of node attributes which should be excluded from export + self.transient_attrs += ['agent_id']; + } + + # creates the subgraph based on data in 'states' + def on_register(){ + agent_node = &self.agent_id; + persona_action = agent_node.get_action(action_label=self.persona_action); + persona_action.import_parameters(self.parameters); + + self.get_agent().get_memory().purge_collection_memory(self.label); + + collection = self.get_collection(); + index = 1; + states_created = {}; + + # initialize initial_state with optional prompt/directive if present + if ("prompt" in self.initial_state and "directive" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + prompt=self.initial_state.get("prompt"), + directive=self.initial_state.get("directive"), + label="initial_state" + ); + } + elif ("prompt" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + prompt=self.initial_state.get("prompt"), + label="initial_state" + ); + } + elif ("directive" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + directive=self.initial_state.get("directive"), + label="initial_state" + ); + } + else { + initial_state = InitialState( + collection_id=collection.id, + label="initial_state" + ); + } + + collection ++> initial_state; + + for state_info in self.states { + states_created[f"state{index}"] = State(state_info=state_info, collection_id=collection.id, label=state_info.get("name", f"state{index}")); + + if(index == 1){ + initial_state ++> states_created[f"state{index}"]; + } + else { + states_created[f"state{index - 1}"] ++> states_created[f"state{index}"]; + } + index += 1; + } + + # initialize completed_state with optional prompt/directive if present + if ("prompt" in self.completed_state and "directive" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + prompt=self.completed_state.get("prompt"), + directive=self.completed_state.get("directive"), + label="completed_state" + ); + } + elif ("prompt" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + prompt=self.completed_state.get("prompt"), + label="completed_state" + ); + } + elif ("directive" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + directive=self.completed_state.get("directive"), + label="completed_state" + ); + } + else { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + label="completed_state" + ); + } + + if ("directive" in self.confirmed_state) { + confirmed_state = ConfirmedState( + collection_id=collection.id, + directive=self.confirmed_state.get("directive"), + label="confirmed_state" + ); + }else{ + confirmed_state = ConfirmedState( + collection_id=collection.id, + label="confirmed_state" + ); + } + + # initialize revision_state with optional prompt/directive if present + if ("prompt" in self.revision_state and "directive" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + prompt=self.revision_state.get("prompt"), + directive=self.revision_state.get("directive"), + label="revision_state" + ); + } + elif ("prompt" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + prompt=self.revision_state.get("prompt"), + label="revision_state" + ); + } + elif ("directive" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + directive=self.revision_state.get("directive"), + label="revision_state" + ); + } + else { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + label="revision_state" + ); + } + + states_created[f"state{index - 1}"] ++> completed_state; + completed_state ++> confirmed_state; + completed_state ++> revision_state; + + self.logger.info(f"Created states in subgraph"); + } + + def run(frame_node:Frame, interaction_node:Interaction) -> str { + directives = []; + pre_process = self.pre_process(frame_node, interaction_node); + if type(pre_process) is str{ + directives = [pre_process]; + } + elif type(pre_process) is list{ + directives = pre_process; + } + walker_result = self.walk_subgraph(frame_node); + directives.extend(walker_result.directives); + + walker_result.directives = {}; + + responses = frame_node.data_get(key=f"{self.label}_results") or {}; + + custom_directives = self.process_response(responses, frame_node, interaction_node); + if custom_directives{ + directives = (custom_directives); + } + + if (responses.get("confirm_response", "")){ + frame_node.data_set(key=f"{self.label}_results", value={}); + } + return directives; + } + + def set_directives(directive:str){ + self.directives.append(directive); + } + + def walk_subgraph(frame_node:Frame) -> Union[str, dict] { + collection = self.get_collection(); + walker_result = collection spawn _subgraph(frame_node=frame_node, agent_node=&self.agent_id, action_label=self.label); + return walker_result; + } + + def update_response(responses:dict, frame:Frame) { + stored_responses = frame.data_get(key=f"{self.label}_results"); + + if type(stored_responses) is not dict or not stored_responses { + stored_responses = {}; + } + + # Merge new responses into stored_responses + for (key, value) in responses.items() { + stored_responses[key] = value; + } + + frame.data_set(key=f"{self.label}_results", value=stored_responses); + } + + def get_states(){ + collection = self.get_collection(); + + states_list = node_get({ + "archetype.collection_id": collection.id, + }); + return states_list; + } + + def list_states(page:int=1, limit:int=20) -> list[dict]{ + collection = self.get_collection(); + + # Initialize pager + pager = NodePager(NodeAnchor.Collection, page_size=limit, current_page=page); + + # Get a page of results + items = pager.get_page({ + "$or": [ + {"$and": [{"name": "InitialState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "State"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "CompletedState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "ConfirmedState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "RevisionState"}, {"archetype.collection_id": collection.id}]} + ] + }); + + if not items { + return {}; + } + + # call export on each and convert it to a dict + items = [item.export() for item in items]; + + # get all info as a dict + pagination_info = pager.to_dict(); + + return { + "page": page, + "limit": limit, + "items": items + }; + } + + def get_state(label:str){ + collection = self.get_collection(); + + state_node = node_obj(node_get({ + "archetype.collection_id": collection.id, + "archetype.label": label + })); + + return state_node; + } + + def update_state(id:str, data:dict) -> GraphNode { + collection = self.get_collection(); + + state_node = node_obj(node_get({ + "archetype.collection_id": collection.id, + "archetype.id": id + })); + + # updates an state node; expects a dict of attribute names mapped to values for updating + # overridden to respond to enable / disable updates + enabled_changed = False; + non_enabled_changed = False; + + if (data) { + for attr in data.keys() { + if (attr not in state_node.protected_attrs) { + # check if attribute is a node attribute + if (hasattr(state_node, attr)) { + # handle changes in disabled/enabled status + if attr == 'enabled' { + current_val = getattr(state_node, 'enabled'); + if current_val != data[attr] { + enabled_changed = True; + if data[attr] == True { + state_node.on_enable(); + } else { + state_node.on_disable(); + } + setattr(state_node, attr, data[attr]); + } + } else { + current_val = getattr(state_node, attr); + if current_val != data[attr] { + non_enabled_changed = True; + } + setattr(state_node, attr, data[attr]); + } + } else { + # Handle context attributes; only mark as changed if the value is new or different + if (attr in state_node._context) { + if state_node._context[attr] != data[attr] { + non_enabled_changed = True; + state_node._context[attr] = data[attr]; + } + } else { + non_enabled_changed = True; + state_node._context[attr] = data[attr]; + } + } + } + } + } + + # Conditionally trigger post_update only when: + # 1. Node is enabled AND + # 2. There were non-enabled changes OR no enabled changes occurred + if state_node.enabled and (non_enabled_changed or not enabled_changed) { + state_node.post_update(); + } + + return state_node; + } + + def abort_process(value:bool = True){ + initial_state = self.get_state("initial_state"); + initial_state.abort = value; + } + + def process_response(responses:dict, frame_node:Frame, interaction_node:Interaction) abs; + + def pre_process(frame_node:Frame, interaction_node:Interaction) abs; + +} + +walker _subgraph { + # set up logger + static has logger: Logger = logging.getLogger(__name__); + + has directives: list = []; + has action_label: str = ""; + has agent_node: GraphNode = None; + has frame_node: Frame = None; + + obj __specs__ { + static has private:bool = True; + } + + can on_collection with Collection entry { + visit [-->] (`?State) else { + disengage; + } + } + + can on_state with State entry { + if here.touch(self) and here.enabled { + # execute the action + response = here.run(self); + if(type(response) is bool) { + visit [-->] else { + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + else { + visit [-->] else { + disengage; + } + } + } + + can on_completed_state with CompletedState entry { + if here.touch(self) { + # execute the action + response=here.run(self); + if response == True { + visit[-->](`?ConfirmedState) else { + disengage; + } + } + elif response == False{ + visit[-->](`?RevisionState)else{ + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + } + can on_confirmed_state with ConfirmedState entry{ + response = here.run(self); + self.set_directives(response); + } + def set_directives(directive:str){ + self.directives.append(directive); + } + can on_revision_state with RevisionState entry{ + if here.touch(self) { + # execute the action + response=here.run(self); + if response is True { + visit [<--](`?CompletedState) else { + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + } + can on_exit with exit { + } + def set_directives(directive:str){ + self.directives.append(directive); + } +} diff --git a/core/jivas/agent/action/subgraph_action/update_state.jac b/core/jivas/agent/action/subgraph_action/update_state.jac new file mode 100644 index 0000000..c8b0072 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/update_state.jac @@ -0,0 +1,43 @@ +import logging; +import from logging { Logger } +import from jivas.agent.core.agent { Agent } +import from jivas.agent.action.action { Action } +import from jivas.agent.action.actions { Actions } +import from jivas.agent.action.subgraph_action.subgraph_action { SubgraphAction } +import from jivas.agent.action.agent_graph_walker { agent_graph_walker } + + +walker update_state(agent_graph_walker) { + # action endpoint for updating states in a subgraph + + has state_id:str = ""; + has data:dict = {}; + has response:dict = {}; + has reporting:bool = True; + has label:str = ""; + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + class __specs__ { + static has private: bool = False; + static has excluded: list[str] = ["response"]; # exclude response from the specs + } + + can on_agent with Agent entry { + visit [-->](`?Actions); + } + + can on_actions with Actions entry { + visit [-->](`?SubgraphAction)(?enabled==True)(?label==self.label); + } + + + can on_action with Action entry { + self.response = here.update_state(id = self.state_id, data = self.data); + + if self.reporting { + report self.response; + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/analytics/get_interaction_logs.jac b/core/jivas/agent/analytics/get_interaction_logs.jac index b327134..750fa9d 100644 --- a/core/jivas/agent/analytics/get_interaction_logs.jac +++ b/core/jivas/agent/analytics/get_interaction_logs.jac @@ -13,6 +13,7 @@ walker get_interaction_logs(agent_graph_walker) { has start_date: str = ""; has end_date: str = ""; has session_id: str = ""; + has frame_id: str = ""; has channel: str = ""; has timezone: str = "UTC"; has page: int = 1; @@ -40,8 +41,12 @@ walker get_interaction_logs(agent_graph_walker) { } }; + if self.frame_id { + match_criteria["frame_id"] = self.frame_id; + } + if self.session_id { - match_criteria["session_id"] = self.session_id; + match_criteria["response.session_id"] = self.session_id; } if self.channel { diff --git a/core/jivas/agent/lib.jac b/core/jivas/agent/lib.jac index e671644..cc5ca94 100644 --- a/core/jivas/agent/lib.jac +++ b/core/jivas/agent/lib.jac @@ -25,6 +25,11 @@ import from jivas.agent.action { stt } +import from jivas.agent.action.subgraph_action { + list_states, + update_state +} + import from jivas.agent.memory { add_frame, get_frames, diff --git a/core/jivas/agent/memory/frame.jac b/core/jivas/agent/memory/frame.jac index d16c0d8..93df73d 100644 --- a/core/jivas/agent/memory/frame.jac +++ b/core/jivas/agent/memory/frame.jac @@ -7,7 +7,6 @@ import from jivas.agent.modules.text.parsing { extract_first_name } import from jivas.agent.memory.interaction { Interaction } import from jivas.agent.memory.interaction_response { InteractionResponse, TextInteractionMessage } import from jivas.agent.core.graph_node { GraphNode } -import from jivas.agent.action.interact_action { InteractAction } import from jac_cloud.plugin.jaseci { JacPlugin as Jac } node Frame(GraphNode) { diff --git a/core/setup.py b/core/setup.py index 5dded2d..f73376f 100644 --- a/core/setup.py +++ b/core/setup.py @@ -42,9 +42,9 @@ def get_version() -> str: package_data={"jivas": []}, python_requires=">=3.12.0", install_requires=[ - "jvcli<=2.1.20", - "jvclient<=2.1.20", - "jvserve<=2.1.20", + "jvcli", + "jvclient", + "jvserve", "jac-cloud==0.2.7", "jaclang==0.8.7", "pytz==2025.2", diff --git a/jvcli/jvcli/templates/2.1.22/project/README.md b/jvcli/jvcli/templates/2.1.23/project/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/README.md rename to jvcli/jvcli/templates/2.1.23/project/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/actions/README.md b/jvcli/jvcli/templates/2.1.23/project/actions/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/actions/README.md rename to jvcli/jvcli/templates/2.1.23/project/actions/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/daf/README.md b/jvcli/jvcli/templates/2.1.23/project/daf/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/daf/README.md rename to jvcli/jvcli/templates/2.1.23/project/daf/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/env.example b/jvcli/jvcli/templates/2.1.23/project/env.example similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/env.example rename to jvcli/jvcli/templates/2.1.23/project/env.example diff --git a/jvcli/jvcli/templates/2.1.22/project/gitignore.example b/jvcli/jvcli/templates/2.1.23/project/gitignore.example similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/gitignore.example rename to jvcli/jvcli/templates/2.1.23/project/gitignore.example diff --git a/jvcli/jvcli/templates/2.1.22/project/globals.jac b/jvcli/jvcli/templates/2.1.23/project/globals.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/globals.jac rename to jvcli/jvcli/templates/2.1.23/project/globals.jac diff --git a/jvcli/jvcli/templates/2.1.22/project/main.jac b/jvcli/jvcli/templates/2.1.23/project/main.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/main.jac rename to jvcli/jvcli/templates/2.1.23/project/main.jac diff --git a/jvcli/jvcli/templates/2.1.22/project/tests/README.md b/jvcli/jvcli/templates/2.1.23/project/tests/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/tests/README.md rename to jvcli/jvcli/templates/2.1.23/project/tests/README.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/CHANGELOG.md b/jvcli/jvcli/templates/2.1.23/sourcefiles/CHANGELOG.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/CHANGELOG.md rename to jvcli/jvcli/templates/2.1.23/sourcefiles/CHANGELOG.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/README.md b/jvcli/jvcli/templates/2.1.23/sourcefiles/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/README.md rename to jvcli/jvcli/templates/2.1.23/sourcefiles/README.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_app.py b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_app.py similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_app.py rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_app.py diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_archetype.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_archetype.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_archetype.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_archetype.jac diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_info.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_info.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_info.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_info.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_lib.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_lib.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_lib.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_lib.jac diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_descriptor.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_descriptor.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_descriptor.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_descriptor.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_info.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_info.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_info.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_info.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_knowledge.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_knowledge.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_knowledge.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_knowledge.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_memory.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_memory.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_memory.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_memory.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/interact_action_archetype.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/interact_action_archetype.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/interact_action_archetype.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/interact_action_archetype.jac