Skip to content

Commit

Permalink
Add command to update the action packages (also call it on create).
Browse files Browse the repository at this point in the history
  • Loading branch information
Ovidiu Rusu authored and fabioz committed Sep 13, 2024
1 parent 90db2c1 commit 6e0edb2
Show file tree
Hide file tree
Showing 15 changed files with 297 additions and 52 deletions.
17 changes: 9 additions & 8 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
## Unreleased

- New Command: `Open Agent Runbook`.
- New command: `Sema4.ai: Import Agent Package (Zip)`.
- New Command: `Sema4.ai: Open Agent Runbook`.
- When an action package is created inside an agent, the agent spec is updated accordingly.
- Renamed Commands:
- Open Agent Spec (agent-spec.yaml) → Configure Agent
- Configure Action Package (package.yaml) → Configure Action Package
- UI Update: Moved `Configure Agent` and `Configure Action Package` from the `Commands` section to the top level of the agent/action.
- Hover for `agent-spec.yaml` (using new spec format for v2)
- Code analysis for `agent-spec.yaml` (using new spec format for v2)
- Basic analysis for the full spec based on paths/types
- Checks if the `zip`, `folder` type matches the actual referenced type
- Checks if the action package `name` matches the referenced value
- Checks if the action package `version` matches the referenced value
- Checks that all the actions found under `actions` are properly referenced in the `agent-spec.yaml`
- Note: handles both .zip and expanded action packages.
- New dependency required: `tree-sitter`
- Update `robocorp-trustore` dependency to 0.9.1
- No longer use `requests` (due to breakage with truststore)
Expand All @@ -22,13 +30,6 @@
- Accept OAuth2 without `clientSecret` (using `pkce` flow).
- The yaml for `OAuth2` is now different (saved in new location with new structure).
- There's now a `mode` which can specify `mode: sema4ai` to use `Sema4.ai` provided configuration (without `clientSecret` required).
- New command: `Sema4.ai: Import Agent Package (Zip)`.
- Additional validations in the `agent-spec.yaml` to verify that the information is in sync with linked action packages:
- Check if the `zip`, `folder` type matches the actual referenced type
- Check if the action package `name` matches the referenced value
- Check if the action package `version` matches the referenced value
- Checks that all the actions found under `actions` are properly referenced in the `agent-spec.yaml`
- Note: handles both .zip and expanded action packages.

## New in 2.4.2 (2024-08-26)

Expand Down
6 changes: 6 additions & 0 deletions sema4ai/codegen/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,12 @@ def __init__(
server_handled=False,
hide_from_command_palette=False,
),
Command(
"sema4ai.refreshAgentSpec.internal",
"Refresh Agent Spec (internal)",
add_to_package_json=False,
server_handled=True,
),
]


Expand Down
1 change: 1 addition & 0 deletions sema4ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"onCommand:sema4ai.getRunbookPathFromAgentSpec.internal",
"onCommand:sema4ai.agentPackagePublishToSema4AIStudioApp",
"onCommand:sema4ai.agentPackageImport",
"onCommand:sema4ai.refreshAgentSpec.internal",
"onDebugInitialConfigurations",
"onDebugResolve:sema4ai",
"onView:sema4ai-task-packages-tree",
Expand Down
2 changes: 1 addition & 1 deletion sema4ai/src/sema4ai_code/action_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ def get_error_message(result_message: str) -> str:
with open(package_path, "w") as file:
yaml.dump(package_yaml, file)

return ActionResult(success=True, message=None)
return ActionResult(success=True, message=None, result=package_path)
else:
return ActionResult(
success=False, message=get_error_message(command_result.message or "")
Expand Down
10 changes: 6 additions & 4 deletions sema4ai/src/sema4ai_code/agents/agent_spec_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ def _verify_yaml_matches_spec(
):
version_in_filesystem = (
package_info_in_filesystem.get_version()
or "Unable to get version from package.yaml"
)
version_in_agent_package = self._get_value_text(yaml_node)
if version_in_filesystem != version_in_agent_package:
Expand All @@ -649,7 +650,10 @@ def _verify_yaml_matches_spec(
spec_node.data.expected_type.expected_type
== _ExpectedTypeEnum.action_package_name_link
):
name_in_filesystem = package_info_in_filesystem.get_name()
name_in_filesystem = (
package_info_in_filesystem.get_name()
or "Unable to get name from package.yaml"
)
name_in_agent_package = self._get_value_text(yaml_node)
if name_in_filesystem != name_in_agent_package:
yield Error(
Expand Down Expand Up @@ -735,9 +739,7 @@ def _verify_yaml_matches_spec(
node=yaml_node.data.node,
)
else:
relative_to: Optional[
str
] = spec_node.data.expected_type.relative_to
relative_to: str | None = spec_node.data.expected_type.relative_to
assert (
relative_to
), f"Expected relative_to to be set in {spec_node.data.path}"
Expand Down
26 changes: 17 additions & 9 deletions sema4ai/src/sema4ai_code/agents/list_actions_from_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@dataclass
class ActionPackageInFilesystem:
relative_path: str
organization: str

package_yaml_path: Optional[Path] = None
zip_path: Optional[Path] = None
Expand Down Expand Up @@ -73,21 +74,23 @@ def get_as_dict(self) -> dict:
self._loaded_yaml_error = str(e)
raise

def get_version(self) -> str:
def get_version(self) -> str | None:
try:
contents = self.get_as_dict()
except Exception as e:
return str(e)
except Exception:
return None

return str(contents.get("version", "Unable to get version from package.yaml"))
version = contents.get("version")
return str(version) if version is not None else None

def get_name(self) -> str:
def get_name(self) -> str | None:
try:
contents = self.get_as_dict()
except Exception as e:
return str(e)
except Exception:
return None

return str(contents.get("name", "Unable to get name from package.yaml"))
name = contents.get("name")
return str(name) if name is not None else None


def list_actions_from_agent(
Expand All @@ -109,19 +112,24 @@ def list_actions_from_agent(
for package_yaml in actions_dir.rglob("package.yaml"):
package_yaml = package_yaml.absolute()
relative_path: str = package_yaml.parent.relative_to(actions_dir).as_posix()
organization = package_yaml.relative_to(actions_dir).parts[0]
found[package_yaml] = ActionPackageInFilesystem(
package_yaml_path=package_yaml, relative_path=relative_path
package_yaml_path=package_yaml,
relative_path=relative_path,
organization=organization,
)

for zip_path in actions_dir.rglob("*.zip"):
zip_path = zip_path.absolute()
package_yaml_contents = get_package_yaml_from_zip(zip_path)
relative_path = zip_path.relative_to(actions_dir).as_posix()
organization = zip_path.relative_to(actions_dir).parts[0]
found[zip_path] = ActionPackageInFilesystem(
package_yaml_path=None,
zip_path=zip_path,
package_yaml_contents=package_yaml_contents,
relative_path=relative_path,
organization=organization,
)

return found
Expand Down
2 changes: 2 additions & 0 deletions sema4ai/src/sema4ai_code/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
SEMA4AI_GET_RUNBOOK_PATH_FROM_AGENT_SPEC_INTERNAL = "sema4ai.getRunbookPathFromAgentSpec.internal" # Get Runbook Path from Agent Spec (internal)
SEMA4AI_AGENT_PACKAGE_PUBLISH_TO_SEMA4_AI_STUDIO_APP = "sema4ai.agentPackagePublishToSema4AIStudioApp" # Publish Agent Package to Sema4.ai Studio
SEMA4AI_AGENT_PACKAGE_IMPORT = "sema4ai.agentPackageImport" # Import Agent Package (Zip)
SEMA4AI_REFRESH_AGENT_SPEC_INTERNAL = "sema4ai.refreshAgentSpec.internal" # Refresh Agent Spec (internal)

ALL_SERVER_COMMANDS = [
SEMA4AI_GET_PLUGINS_DIR,
Expand Down Expand Up @@ -193,6 +194,7 @@
SEMA4AI_CREATE_AGENT_PACKAGE_INTERNAL,
SEMA4AI_PACK_AGENT_PACKAGE_INTERNAL,
SEMA4AI_GET_RUNBOOK_PATH_FROM_AGENT_SPEC_INTERNAL,
SEMA4AI_REFRESH_AGENT_SPEC_INTERNAL,
]

# fmt: on
2 changes: 1 addition & 1 deletion sema4ai/src/sema4ai_code/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class CreateAgentPackageParamsDict(TypedDict):
name: str


class GetRunbookPathFromAgentSpecDict(TypedDict):
class AgentSpecPathDict(TypedDict):
agent_spec_path: str


Expand Down
117 changes: 117 additions & 0 deletions sema4ai/src/sema4ai_code/refresh_agent_spec_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import typing
from pathlib import Path
from typing import TypedDict

if typing.TYPE_CHECKING:
from sema4ai_code.agents.list_actions_from_agent import ActionPackageInFilesystem

SEMA4AI = "sema4ai"


class ActionPackage(TypedDict, total=False):
name: str | None
organization: str
type: str
version: str | None
whitelist: str
path: str
seen: bool


def _update_whitelist(
action_package: ActionPackage,
whitelist_map: dict[str, ActionPackage],
match_key: str,
) -> bool:
found_package = whitelist_map.get(match_key)

if found_package and not found_package.get("seen"):
action_package["whitelist"] = found_package["whitelist"]
found_package["seen"] = True
return True

return False


def _update_agent_spec_with_actions(
agent_spec: dict,
action_packages_in_filesystem: list["ActionPackageInFilesystem"],
whitelist_map: dict[str, ActionPackage],
) -> None:
new_action_packages: list[ActionPackage] = [
{
"name": action_package.get_name(),
"organization": action_package.organization,
"version": action_package.get_version(),
"path": action_package.relative_path,
"type": "zip" if action_package.zip_path else "folder",
"whitelist": "",
}
for action_package in action_packages_in_filesystem
if action_package.organization.replace(".", "").lower() != SEMA4AI
]
missing = []

# First try to match by path.
for action_package in new_action_packages:
if not _update_whitelist(
action_package,
whitelist_map,
match_key=action_package["path"],
):
missing.append(action_package)

# Couldn't find a path match, try to match by name.
for action_package in missing:
if action_package["name"]:
_update_whitelist(
action_package, whitelist_map, match_key=action_package["name"]
)

# If there was a whitelisted action and it wasn't matched, keep the old config
# around so that the user can fix it.
for whitelisted_action in whitelist_map.values():
if not whitelisted_action.pop("seen", False):
new_action_packages.append(whitelisted_action)

agent_spec["agent-package"]["agents"][0]["action-packages"] = new_action_packages


def _create_whitelist_mapping(agent_spec: dict) -> dict[str, ActionPackage]:
whitelist_mapping = {}

for action_package in agent_spec["agent-package"]["agents"][0].get(
"action-packages", []
):
if action_package.get("whitelist"):
if action_package.get("name"):
whitelist_mapping[action_package["name"]] = action_package

if action_package.get("path"):
whitelist_mapping[action_package["path"]] = action_package

return whitelist_mapping


def update_agent_spec(agent_spec_path: Path) -> None:
from ruamel.yaml import YAML

from sema4ai_code.agents.list_actions_from_agent import list_actions_from_agent

yaml = YAML()
yaml.preserve_quotes = True

with agent_spec_path.open("r") as file:
agent_spec = yaml.load(file)

action_packages_in_filesystem = list(
list_actions_from_agent(agent_spec_path.parent).values()
)
current_whitelist_mapping = _create_whitelist_mapping(agent_spec)

_update_agent_spec_with_actions(
agent_spec, action_packages_in_filesystem, current_whitelist_mapping
)

with agent_spec_path.open("w") as file:
yaml.dump(agent_spec, file)
16 changes: 14 additions & 2 deletions sema4ai/src/sema4ai_code/robocorp_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
ActionServerPackageUploadDict,
ActionServerPackageUploadStatusDict,
ActionServerVerifyLoginResultDict,
AgentSpecPathDict,
CloudListWorkspaceDict,
ConfigurationDiagnosticsDict,
CreateActionPackageParamsDict,
CreateAgentPackageParamsDict,
CreateRobotParamsDict,
DownloadToolDict,
GetRunbookPathFromAgentSpecDict,
IRccRobotMetadata,
IRccWorkspace,
ListActionsParams,
Expand All @@ -60,6 +60,7 @@
WorkItem,
WorkspaceInfoDict,
)
from sema4ai_code.refresh_agent_spec_helper import update_agent_spec
from sema4ai_code.vendored_deps.package_deps._deps_protocols import (
ICondaCloud,
IPyPiCloud,
Expand Down Expand Up @@ -1700,7 +1701,7 @@ def _pack_agent_package(

@command_dispatcher(commands.SEMA4AI_GET_RUNBOOK_PATH_FROM_AGENT_SPEC_INTERNAL)
def _get_runbook_path_from_agent_spec(
self, params: GetRunbookPathFromAgentSpecDict
self, params: AgentSpecPathDict
) -> ActionResultDict:
agent_spec_path = params["agent_spec_path"]

Expand All @@ -1722,6 +1723,17 @@ def _get_runbook_path_from_agent_spec(
result=runbook_path,
).as_dict()

@command_dispatcher(commands.SEMA4AI_REFRESH_AGENT_SPEC_INTERNAL)
def _refresh_agent_spec(self, params: AgentSpecPathDict) -> ActionResultDict:
try:
update_agent_spec(Path(params["agent_spec_path"]))
except Exception as e:
return ActionResult(
success=False, message=f"Failed to refresh the agent configuration: {e}"
).as_dict()

return ActionResult(success=True, message=None).as_dict()

def _pack_agent_package_threaded(self, directory, ws, monitor: IMonitor):
from sema4ai_ls_core.progress_report import progress_context

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
start:
character: 10
line: 14
severity: 1
severity: 2
source: sema4ai
Loading

0 comments on commit 6e0edb2

Please sign in to comment.