solace-agent-mesh 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/__init__.py +0 -3
- solace_agent_mesh/agents/__init__.py +0 -0
- solace_agent_mesh/agents/base_agent_component.py +224 -0
- solace_agent_mesh/agents/global/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/agent_state_change.py +54 -0
- solace_agent_mesh/agents/global/actions/clear_history.py +32 -0
- solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +160 -0
- solace_agent_mesh/agents/global/actions/create_file.py +70 -0
- solace_agent_mesh/agents/global/actions/error_action.py +45 -0
- solace_agent_mesh/agents/global/actions/plantuml_diagram.py +93 -0
- solace_agent_mesh/agents/global/actions/plotly_graph.py +117 -0
- solace_agent_mesh/agents/global/actions/retrieve_file.py +51 -0
- solace_agent_mesh/agents/global/global_agent_component.py +38 -0
- solace_agent_mesh/agents/image_processing/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/create_image.py +75 -0
- solace_agent_mesh/agents/image_processing/actions/describe_image.py +115 -0
- solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +23 -0
- solace_agent_mesh/agents/slack/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/post_message.py +177 -0
- solace_agent_mesh/agents/slack/slack_agent_component.py +59 -0
- solace_agent_mesh/agents/web_request/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/do_image_search.py +84 -0
- solace_agent_mesh/agents/web_request/actions/do_news_search.py +47 -0
- solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +34 -0
- solace_agent_mesh/agents/web_request/actions/do_web_request.py +134 -0
- solace_agent_mesh/agents/web_request/actions/download_file.py +69 -0
- solace_agent_mesh/agents/web_request/web_request_agent_component.py +33 -0
- solace_agent_mesh/assets/web-visualizer/assets/index-C5awueeJ.js +109 -0
- solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +1 -0
- solace_agent_mesh/assets/web-visualizer/index.html +14 -0
- solace_agent_mesh/assets/web-visualizer/vite.svg +1 -0
- solace_agent_mesh/cli/__init__.py +1 -0
- solace_agent_mesh/cli/commands/__init__.py +0 -0
- solace_agent_mesh/cli/commands/add/__init__.py +3 -0
- solace_agent_mesh/cli/commands/add/add.py +88 -0
- solace_agent_mesh/cli/commands/add/agent.py +110 -0
- solace_agent_mesh/cli/commands/add/copy_from_plugin.py +90 -0
- solace_agent_mesh/cli/commands/add/gateway.py +221 -0
- solace_agent_mesh/cli/commands/build.py +631 -0
- solace_agent_mesh/cli/commands/chat/__init__.py +3 -0
- solace_agent_mesh/cli/commands/chat/chat.py +361 -0
- solace_agent_mesh/cli/commands/config.py +29 -0
- solace_agent_mesh/cli/commands/init/__init__.py +3 -0
- solace_agent_mesh/cli/commands/init/ai_provider_step.py +76 -0
- solace_agent_mesh/cli/commands/init/broker_step.py +102 -0
- solace_agent_mesh/cli/commands/init/builtin_agent_step.py +88 -0
- solace_agent_mesh/cli/commands/init/check_if_already_done.py +13 -0
- solace_agent_mesh/cli/commands/init/create_config_file_step.py +52 -0
- solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +96 -0
- solace_agent_mesh/cli/commands/init/file_service_step.py +73 -0
- solace_agent_mesh/cli/commands/init/init.py +114 -0
- solace_agent_mesh/cli/commands/init/project_structure_step.py +45 -0
- solace_agent_mesh/cli/commands/init/rest_api_step.py +50 -0
- solace_agent_mesh/cli/commands/init/web_ui_step.py +40 -0
- solace_agent_mesh/cli/commands/plugin/__init__.py +3 -0
- solace_agent_mesh/cli/commands/plugin/add.py +98 -0
- solace_agent_mesh/cli/commands/plugin/build.py +217 -0
- solace_agent_mesh/cli/commands/plugin/create.py +117 -0
- solace_agent_mesh/cli/commands/plugin/plugin.py +109 -0
- solace_agent_mesh/cli/commands/plugin/remove.py +71 -0
- solace_agent_mesh/cli/commands/run.py +68 -0
- solace_agent_mesh/cli/commands/visualizer.py +138 -0
- solace_agent_mesh/cli/config.py +81 -0
- solace_agent_mesh/cli/main.py +306 -0
- solace_agent_mesh/cli/utils.py +246 -0
- solace_agent_mesh/common/__init__.py +0 -0
- solace_agent_mesh/common/action.py +91 -0
- solace_agent_mesh/common/action_list.py +37 -0
- solace_agent_mesh/common/action_response.py +327 -0
- solace_agent_mesh/common/constants.py +3 -0
- solace_agent_mesh/common/mysql_database.py +40 -0
- solace_agent_mesh/common/postgres_database.py +79 -0
- solace_agent_mesh/common/prompt_templates.py +30 -0
- solace_agent_mesh/common/prompt_templates_unused_delete.py +161 -0
- solace_agent_mesh/common/stimulus_utils.py +152 -0
- solace_agent_mesh/common/time.py +24 -0
- solace_agent_mesh/common/utils.py +638 -0
- solace_agent_mesh/configs/agent_global.yaml +74 -0
- solace_agent_mesh/configs/agent_image_processing.yaml +82 -0
- solace_agent_mesh/configs/agent_slack.yaml +64 -0
- solace_agent_mesh/configs/agent_web_request.yaml +75 -0
- solace_agent_mesh/configs/conversation_to_file.yaml +56 -0
- solace_agent_mesh/configs/error_catcher.yaml +56 -0
- solace_agent_mesh/configs/monitor.yaml +0 -0
- solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +106 -0
- solace_agent_mesh/configs/monitor_user_feedback.yaml +58 -0
- solace_agent_mesh/configs/orchestrator.yaml +241 -0
- solace_agent_mesh/configs/service_embedding.yaml +81 -0
- solace_agent_mesh/configs/service_llm.yaml +265 -0
- solace_agent_mesh/configs/visualize_websocket.yaml +55 -0
- solace_agent_mesh/gateway/__init__.py +0 -0
- solace_agent_mesh/gateway/components/__init__.py +0 -0
- solace_agent_mesh/gateway/components/gateway_base.py +41 -0
- solace_agent_mesh/gateway/components/gateway_input.py +265 -0
- solace_agent_mesh/gateway/components/gateway_output.py +289 -0
- solace_agent_mesh/gateway/identity/bamboohr_identity.py +18 -0
- solace_agent_mesh/gateway/identity/identity_base.py +10 -0
- solace_agent_mesh/gateway/identity/identity_provider.py +60 -0
- solace_agent_mesh/gateway/identity/no_identity.py +9 -0
- solace_agent_mesh/gateway/identity/passthru_identity.py +9 -0
- solace_agent_mesh/monitors/base_monitor_component.py +26 -0
- solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +75 -0
- solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +560 -0
- solace_agent_mesh/orchestrator/__init__.py +0 -0
- solace_agent_mesh/orchestrator/action_manager.py +225 -0
- solace_agent_mesh/orchestrator/components/__init__.py +0 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +54 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +179 -0
- solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +107 -0
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +477 -0
- solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +246 -0
- solace_agent_mesh/orchestrator/orchestrator_main.py +166 -0
- solace_agent_mesh/orchestrator/orchestrator_prompt.py +410 -0
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +56 -0
- solace_agent_mesh/services/bamboo_hr_service/__init__.py +3 -0
- solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +182 -0
- solace_agent_mesh/services/common/__init__.py +4 -0
- solace_agent_mesh/services/common/auto_expiry.py +45 -0
- solace_agent_mesh/services/common/singleton.py +18 -0
- solace_agent_mesh/services/file_service/__init__.py +14 -0
- solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
- solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +149 -0
- solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +162 -0
- solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +64 -0
- solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +106 -0
- solace_agent_mesh/services/file_service/file_service.py +432 -0
- solace_agent_mesh/services/file_service/file_service_constants.py +54 -0
- solace_agent_mesh/services/file_service/file_transformations.py +131 -0
- solace_agent_mesh/services/file_service/file_utils.py +322 -0
- solace_agent_mesh/services/file_service/transformers/__init__.py +5 -0
- solace_agent_mesh/services/history_service/__init__.py +3 -0
- solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
- solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +78 -0
- solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +167 -0
- solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +163 -0
- solace_agent_mesh/services/history_service/history_service.py +139 -0
- solace_agent_mesh/services/llm_service/components/llm_request_component.py +293 -0
- solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +152 -0
- solace_agent_mesh/services/middleware_service/__init__.py +0 -0
- solace_agent_mesh/services/middleware_service/middleware_service.py +20 -0
- solace_agent_mesh/templates/action.py +38 -0
- solace_agent_mesh/templates/agent.py +29 -0
- solace_agent_mesh/templates/agent.yaml +70 -0
- solace_agent_mesh/templates/gateway-config-template.yaml +6 -0
- solace_agent_mesh/templates/gateway-default-config.yaml +28 -0
- solace_agent_mesh/templates/gateway-flows.yaml +81 -0
- solace_agent_mesh/templates/gateway-header.yaml +16 -0
- solace_agent_mesh/templates/gateway_base.py +15 -0
- solace_agent_mesh/templates/gateway_input.py +98 -0
- solace_agent_mesh/templates/gateway_output.py +71 -0
- solace_agent_mesh/templates/plugin-pyproject.toml +30 -0
- solace_agent_mesh/templates/rest-api-default-config.yaml +24 -0
- solace_agent_mesh/templates/rest-api-flows.yaml +80 -0
- solace_agent_mesh/templates/slack-default-config.yaml +9 -0
- solace_agent_mesh/templates/slack-flows.yaml +90 -0
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +77 -0
- solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +8 -0
- solace_agent_mesh/templates/web-default-config.yaml +5 -0
- solace_agent_mesh/templates/web-flows.yaml +86 -0
- solace_agent_mesh/tools/__init__.py +0 -0
- solace_agent_mesh/tools/components/__init__.py +0 -0
- solace_agent_mesh/tools/components/conversation_formatter.py +111 -0
- solace_agent_mesh/tools/components/file_resolver_component.py +58 -0
- solace_agent_mesh/tools/config/runtime_config.py +26 -0
- solace_agent_mesh-0.1.1.dist-info/METADATA +179 -0
- solace_agent_mesh-0.1.1.dist-info/RECORD +174 -0
- solace_agent_mesh-0.1.1.dist-info/entry_points.txt +3 -0
- solace_agent_mesh-0.0.1.dist-info/licenses/LICENSE.txt → solace_agent_mesh-0.1.1.dist-info/licenses/LICENSE +1 -2
- solace_agent_mesh-0.0.1.dist-info/METADATA +0 -51
- solace_agent_mesh-0.0.1.dist-info/RECORD +0 -5
- {solace_agent_mesh-0.0.1.dist-info → solace_agent_mesh-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
import yaml
|
|
3
|
+
from langchain_core.messages import HumanMessage
|
|
4
|
+
from ..services.file_service import FS_PROTOCOL, Types, LLM_QUERY_OPTIONS, TRANSFORMERS
|
|
5
|
+
|
|
6
|
+
# Cap the number of examples so we don't overwhelm the LLM
|
|
7
|
+
MAX_SYSTEM_PROMPT_EXAMPLES = 6
|
|
8
|
+
|
|
9
|
+
# Examples that should always be included in the prompt
|
|
10
|
+
fixed_examples = [
|
|
11
|
+
""" <example>
|
|
12
|
+
<example_docstring>
|
|
13
|
+
This example shows a stimulus from a chatbot gateway in which a user is asking about the top stories on the website hacker news. The web_request is not yet open, so the change_agent_status action is invoked to open the web_request agent.
|
|
14
|
+
</example_docstring>
|
|
15
|
+
<example_stimulus>
|
|
16
|
+
<{tp}stimulus starting_id="1"/>
|
|
17
|
+
What is the top story on hacker news?
|
|
18
|
+
</{tp}stimulus>
|
|
19
|
+
<{tp}stimulus_metadata>
|
|
20
|
+
local_time: 2024-11-06 15:58:04 EST-0500 (Wednesday)
|
|
21
|
+
</{tp}stimulus_metadata>
|
|
22
|
+
</example_stimulus>
|
|
23
|
+
<example_response>
|
|
24
|
+
<{tp}reasoning>
|
|
25
|
+
- User is asking for the top story on Hacker News
|
|
26
|
+
- We need to use the web_request agent to fetch the latest information
|
|
27
|
+
- The web_request agent is currently closed, so we need to open it first
|
|
28
|
+
- After opening the agent, we\'ll need to make a web request to Hacker News
|
|
29
|
+
</{tp}reasoning>
|
|
30
|
+
|
|
31
|
+
<{tp}status_update>To get the latest top story from Hacker News, I\'ll need to access the web. I\'m preparing to do that now.</{tp}status_update>
|
|
32
|
+
|
|
33
|
+
<{tp}invoke_action agent="global" action="change_agent_status">
|
|
34
|
+
<{tp}parameter name="agent_name">web_request</{tp}parameter>
|
|
35
|
+
<{tp}parameter name="new_state">open</{tp}parameter>
|
|
36
|
+
</{tp}invoke_action>
|
|
37
|
+
</example_response>
|
|
38
|
+
</example>
|
|
39
|
+
"""
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_file_handling_prompt(tp: str) -> str:
|
|
44
|
+
parameters_desc = ""
|
|
45
|
+
parameter_examples = ""
|
|
46
|
+
|
|
47
|
+
for transformer in TRANSFORMERS:
|
|
48
|
+
if transformer.description:
|
|
49
|
+
parameters_desc += "\n" + transformer.description.strip() + "\n"
|
|
50
|
+
|
|
51
|
+
if transformer.examples:
|
|
52
|
+
for example in transformer.examples:
|
|
53
|
+
parameter_examples += "\n" + example.strip() + "\n"
|
|
54
|
+
|
|
55
|
+
parameters_desc = "\n ".join(parameters_desc.split("\n"))
|
|
56
|
+
parameter_examples = "\n ".join(parameter_examples.split("\n"))
|
|
57
|
+
|
|
58
|
+
parameters_desc = parameters_desc.replace("{tp}", tp)
|
|
59
|
+
parameter_examples = parameter_examples.replace("{tp}", tp)
|
|
60
|
+
|
|
61
|
+
if parameter_examples:
|
|
62
|
+
parameter_examples = f"""
|
|
63
|
+
Here are some examples of how to use the query parameters:
|
|
64
|
+
{parameter_examples}"""
|
|
65
|
+
|
|
66
|
+
prompt = f"""
|
|
67
|
+
XML tags are used to represent files. The assistant will use the <{tp}file> tag to represent a file. The file tag has the following format:
|
|
68
|
+
<{tp}file name="filename" mime_type="mimetype" size="size in bytes">
|
|
69
|
+
<schema-yaml>...JSON schema, yaml format...</schema-yaml> (optional)
|
|
70
|
+
<shape>...textual description of the shape of the data (type dependent)...</shape> (optional)
|
|
71
|
+
<url> URL to download the file </url> or <data> The data content if it's small </data>
|
|
72
|
+
<data-source> the origin of the data </data-source> (optional)
|
|
73
|
+
</{tp}file>
|
|
74
|
+
|
|
75
|
+
- schema-yaml is optional, but when present it gives insight into the structure of the data in the file. It can be used to create URL query parameters.
|
|
76
|
+
- shape is optional, but when present it gives contextual information about the content.
|
|
77
|
+
- A file either has a URL or data content, but not both. The URL can be used to download the file content. You can not create a new URL.
|
|
78
|
+
|
|
79
|
+
Here's an example of a file with URL.
|
|
80
|
+
|
|
81
|
+
<{tp}file name="filename.csv" mime_type="text/csv" size="1024">
|
|
82
|
+
<url>{FS_PROTOCOL}://da2b679f-4474-4350-92c9-89c9691ab902_filename.csv</url>
|
|
83
|
+
<schema-yaml>
|
|
84
|
+
type: {Types.ARRAY}
|
|
85
|
+
items:
|
|
86
|
+
type: {Types.OBJECT}
|
|
87
|
+
properties:
|
|
88
|
+
id:
|
|
89
|
+
type: {Types.INT}
|
|
90
|
+
firstname:
|
|
91
|
+
type: {Types.STR}
|
|
92
|
+
lastname:
|
|
93
|
+
type: {Types.STR}
|
|
94
|
+
age:
|
|
95
|
+
type: {Types.INT}
|
|
96
|
+
</schema-yaml>
|
|
97
|
+
<shape>120 rows x 4 columns</shape>
|
|
98
|
+
</{tp}file>
|
|
99
|
+
|
|
100
|
+
The assistant can partially load the data by using query parameters in the URL. These parameters are file specific.
|
|
101
|
+
{parameters_desc}
|
|
102
|
+
For all files, there's `encoding` parameter, this is used to encode the file format. The supported values are `datauri`, `base64`, `zip`, and `gzip`. Use this to convert a file to ZIP or datauri for HTML encoding.
|
|
103
|
+
For example (return a file as zip): `{FS_PROTOCOL}://c27e6908-55d5-4ce0-bc93-a8e28f84be12_annual_report.csv?encoding=zip&resolve=true`
|
|
104
|
+
Example 2 (HTML tag must be datauri encoded): `<img src="{FS_PROTOCOL}://c9183b0f-fd11-48b4-ad8f-df221bff3da9_generated_image.png?encoding=datauri&resolve=true" alt="image">`
|
|
105
|
+
|
|
106
|
+
For all files, there's `resolve=true` parameter, this is used to resolve the url to the actual data before processing.
|
|
107
|
+
For example: `{FS_PROTOCOL}://577c928c-c126-42d8-8a48-020b93096110_names.csv?resolve=true`
|
|
108
|
+
|
|
109
|
+
If the assistant needs to only send the value of the data to an agent or the gateway, it can use the `resolve` parameter which will replace the whole url with the data content. No quotation is required when using the `resolve` parameter.
|
|
110
|
+
|
|
111
|
+
For the URLs that have spaces in the query parameters wrap the URL in `<url>` tags.
|
|
112
|
+
|
|
113
|
+
The assistant can either pass the whole file block or just the URL to the gateway or the agent depending on the context and specified requirements.
|
|
114
|
+
When using a {FS_PROTOCOL} URL outside of a file block, always include the 'resolve=true' query parameter to ensure the URL is resolved to the actual data.
|
|
115
|
+
|
|
116
|
+
When talking about a file, use the file name instead of the URL. The URL should be used when trying to access the file.
|
|
117
|
+
|
|
118
|
+
If you need to directly access the content of a text file, use the `retrieve_file` action from the global agent. The action will return the content of the file in the response. If you don't need the content or if you think the user query can be answered with query parameters (eg jq), you can send the url back to the gateway with 'resolve=true'.
|
|
119
|
+
|
|
120
|
+
To create a new persistent file, use the `create_file` action from the global agent. The action will create a new file with the specified content and return the file block in the response. Don't create a file from a URL file block, just use the reference to the file block. Avoid using `create_file` action as much as possible. Prioritize using the query parameters to format the file.
|
|
121
|
+
|
|
122
|
+
If you need to create a non-persistent temporary file, you should use the <data> tag to represent the content of the file.
|
|
123
|
+
For example, if the assistant needs to create a CSV file with the following content:
|
|
124
|
+
id,firstname,lastname,age
|
|
125
|
+
1,John,Doe,30
|
|
126
|
+
2,Jane,Doe,25
|
|
127
|
+
3,James,Smith,40
|
|
128
|
+
|
|
129
|
+
The assistant can use the following response:
|
|
130
|
+
<{tp}file name="employee_info.csv" mime_type="text/csv">
|
|
131
|
+
<data>id,firstname,lastname,age\n1,John,Doe,30\n2,Jane,Doe,25\n3,James,Smith,40\n</data>
|
|
132
|
+
</{tp}file>
|
|
133
|
+
|
|
134
|
+
Note: You can't nest `<{tp}file>` tags inside the `<data>` tag. If you need to address another file within the data, use the URL with the `resolve=true` query parameter.
|
|
135
|
+
|
|
136
|
+
Try avoid creating a persistent file if you can respond with file block with data tags.
|
|
137
|
+
When responding to the gateway, both <url>...</url> or <data>...</data> can be used in <{tp}file> elements. If the data is already available as a url, it is preferred to use that url in replying to the gateway. When the url is used, it is acceptable to use query parameters to select a subset of the file. The system will expand the <url> form to actual data before it reaches the gateway.
|
|
138
|
+
|
|
139
|
+
Never return the bare {FS_PROTOCOL} URL (without 'resolve' query parameter) as a link to the gateway. Always put it in at <{tp}file> tag so the system can handle it properly.
|
|
140
|
+
|
|
141
|
+
{parameter_examples}
|
|
142
|
+
"""
|
|
143
|
+
return prompt
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_examples(
|
|
147
|
+
fixed_examples: List[str], agent_examples: List[str], tp: str
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Create examples string with replaced tag prefix placeholders.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
examples: List of example strings containing {tp} placeholders
|
|
153
|
+
tp: Tag prefix to replace placeholders with
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
String containing all examples with replaced placeholders
|
|
157
|
+
"""
|
|
158
|
+
examples = (fixed_examples + agent_examples)[:MAX_SYSTEM_PROMPT_EXAMPLES]
|
|
159
|
+
return "\n".join([example.replace("{tp}", tp) for example in examples])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def SystemPrompt(info: Dict[str, Any], action_examples: List[str]) -> str:
|
|
163
|
+
response_format_prompt = info.get("response_format_prompt", "")
|
|
164
|
+
response_guidelines_prompt = (
|
|
165
|
+
f"<response_guidelines>\nConsider the following when generating a response to the originator:\n"
|
|
166
|
+
f"{response_format_prompt}</response_guidelines>"
|
|
167
|
+
if response_format_prompt
|
|
168
|
+
else ""
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
available_files = ""
|
|
172
|
+
if (
|
|
173
|
+
info.get("available_files")
|
|
174
|
+
and info.get("available_files") is not None
|
|
175
|
+
and len(info["available_files"]) > 0
|
|
176
|
+
):
|
|
177
|
+
blocks = "\n\n".join(info["available_files"])
|
|
178
|
+
available_files = (
|
|
179
|
+
"\n<available_files>\n"
|
|
180
|
+
"The following files are available for access, only use them if needed:\n"
|
|
181
|
+
f"\n{blocks}\n"
|
|
182
|
+
"</available_files>\n"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Merged
|
|
186
|
+
tp = info["tag_prefix"]
|
|
187
|
+
examples = create_examples(fixed_examples, action_examples, tp)
|
|
188
|
+
|
|
189
|
+
handling_files = get_file_handling_prompt(tp)
|
|
190
|
+
|
|
191
|
+
return f"""
|
|
192
|
+
Note to avoid unintended collisions, all tag names in the assistant response will start with the value {tp}
|
|
193
|
+
<orchestrator_info>
|
|
194
|
+
You are an assistant serving as the orchestrator in an AI agentic system. Your primary functions are to:
|
|
195
|
+
1. Receive stimuli from external sources via the system Gateway
|
|
196
|
+
2. Invoke actions within system agents to address these stimuli
|
|
197
|
+
3. Formulate responses based on agent actions
|
|
198
|
+
|
|
199
|
+
This process is iterative, with the assistant being reinvoked at each step.
|
|
200
|
+
|
|
201
|
+
The Stimulus represents user or application requests.
|
|
202
|
+
|
|
203
|
+
The assistant receives a history of all gateway-orchestrator exchanges, excluding its own action invocations and reasoning.
|
|
204
|
+
|
|
205
|
+
The assistant's behavior aligns with the system purpose specified below:
|
|
206
|
+
<system_purpose>
|
|
207
|
+
{info["system_purpose"]}
|
|
208
|
+
</system_purpose>
|
|
209
|
+
<orchestrator_rules>
|
|
210
|
+
The assistant (in the role of orchestrator) will:
|
|
211
|
+
- Manage system agents by opening and closing them as needed:
|
|
212
|
+
1. When opening an agent, its available actions will be listed.
|
|
213
|
+
2. If closed agents are needed for the next step, open only the required agents.
|
|
214
|
+
3. After opening agents, the assistant will be reinvoked with an updated list of open agents and their actions.
|
|
215
|
+
4. When opening an agent, provide only a brief status update without detailed explanations.
|
|
216
|
+
5. Do not perform any other actions besides opening the required agents in this step.
|
|
217
|
+
- Handling stimuli with open agents:
|
|
218
|
+
1. Use agents' actions to break down the stimulus into smaller, manageable tasks.
|
|
219
|
+
2. Prioritize using available actions to fulfill the stimulus whenever possible.
|
|
220
|
+
3. If no suitable agents or actions are available, the assistant will:
|
|
221
|
+
a) Use its own knowledge to respond, or
|
|
222
|
+
b) Ask the user for additional information, or
|
|
223
|
+
c) Inform the user that it cannot fulfill the request.
|
|
224
|
+
- The first user message contains the history of all exchanges between the gateway and the orchestrator before now. Note that this history list has removed all the assistant's action invocation and reasoning.
|
|
225
|
+
- The assistant will not guess at an answer. No answer is better than a wrong answer.
|
|
226
|
+
- The assistant will invoke the actions and specify the parameters for each action, following the rules of the action. If there is not sufficient context to fill in an action parameter, the assistant will ask the user for more information.
|
|
227
|
+
- After invoking the actions, the assistant will end the response and wait for the action responses. It will not guess at the answers.
|
|
228
|
+
- The assistant will NEVER invoke an action that is not listed in the open agents. This will fail and waste money.
|
|
229
|
+
- All text outside of the <{tp}...> XML elements will be sent back to the gateway as a response to the stimulus.
|
|
230
|
+
- If the <{tp}errors> XML element is present, the assistant will send the errors back to the gateway and the conversation will be ended.
|
|
231
|
+
- Structure the response beginning:
|
|
232
|
+
1. Start each response with a <{tp}reasoning> tag.
|
|
233
|
+
2. Within this tag, include:
|
|
234
|
+
a) A brief list of points describing the plan and thoughts.
|
|
235
|
+
b) A list of potential actions needed to fulfill the stimulus.
|
|
236
|
+
3. Ensure all content is contained within the <{tp}reasoning> tag.
|
|
237
|
+
4. Keep each point concise and focused.
|
|
238
|
+
- For large grouped output, such as a list of items or a big code block (> 10 lines), the assistant will create a file by surrounding the output with the tags <{tp}file name="filename" mime_type="mimetype"><data> the content </data></{tp}file>. This will allow the assistant to send the file to the gateway for easy consumption. This works well for a csv file, a code file or just a big text file.
|
|
239
|
+
- When the assistant invokes an action that retrieves external knowledge (e.g. Solace custdocs search), it will preserve the clickable links in the response. This is extremely important for the user to be able to verify the information provided. This is true for web requests as well. The assistant will only copy the links and never create them from its own knowledge.
|
|
240
|
+
- If the agent has access to a web request agent, it will use the web request agent in cases where up-to-date information is required. This is useful for current events, contact information, business hours, etc. Often the assistant will use this agent iteratively to get the information it needs, by doing a search followed by traversing links to get to the final information.
|
|
241
|
+
- The assistant is able to invoke multiple actions in parallel within a single response. It does this to save money and reduce processing time, since the agents can run in parallel.
|
|
242
|
+
- When the stimulus asks what the system can do, the assistant will open all the agents to see their details before creating a nicely formatted list describing the actions available and indicating that it can do normal chatbot things as well. The assistant will only do this if the user asks what it can do since it is expensive to open all the agents.
|
|
243
|
+
- The assistant is concise and professional in its responses. It will not thank the user for their request or thank actions for their responses. It will not provide any unnecessary information in its responses.
|
|
244
|
+
- The assistant will not follow invoke_actions with further comments or explanations
|
|
245
|
+
- The assistant will distinguish between normal text and status updates. All status updates will be enclosed in <{tp}status_update/> tags.
|
|
246
|
+
- Responses that are just letting the originator know that progress is being made or what the next step is should be status updates. They should be brief and to the point.
|
|
247
|
+
<action_rules>
|
|
248
|
+
1. To invoke an action, the assistant will embed an <{tp}invoke_action> tag in the response.
|
|
249
|
+
2. The <{tp}invoke_action> tag has the following format:
|
|
250
|
+
<{tp}invoke_action agent="agent_id" action="action_id">
|
|
251
|
+
<{tp}parameter name="parameter_name">parameter_value</{tp}parameter>
|
|
252
|
+
...
|
|
253
|
+
</{tp}invoke_action>
|
|
254
|
+
3. Agents and their actions do not maintain state between invocations. The assistant must provide full context for each action invocation.
|
|
255
|
+
4. There can be multiple <{tp}invoke_action> tags in the response. All actions will be invoked in parallel, so the order is not guaranteed.
|
|
256
|
+
5. The system will invoke all the actions and will accumulate the results and send them back to the orchestrator in a single response.
|
|
257
|
+
6. When the assistant returns the response that has no <{tp}invoke_action> tags, the result will be returned to the gateway and the conversation will be ended.
|
|
258
|
+
7. Conversation history will be maintained for the full duration of the stimulus processing. This includes all the responses from the assistant and the user.
|
|
259
|
+
8. To open or close an agent, the assistant will invoke the change_agent_status action within the 'global' agent.
|
|
260
|
+
9. The assistant will only choose actions from the list of open agents.
|
|
261
|
+
</action_rules>
|
|
262
|
+
</orchestrator_rules>
|
|
263
|
+
<handling_files>
|
|
264
|
+
{handling_files}
|
|
265
|
+
</handling_files>
|
|
266
|
+
</orchestrator_info>
|
|
267
|
+
|
|
268
|
+
<agents-in-yaml>
|
|
269
|
+
{info["agent_state_yaml"]}
|
|
270
|
+
</agents-in-yaml>
|
|
271
|
+
|
|
272
|
+
<examples>
|
|
273
|
+
{examples}
|
|
274
|
+
</examples>
|
|
275
|
+
|
|
276
|
+
<stimulus_originator_metadata>
|
|
277
|
+
{info["originator_info_yaml"]}
|
|
278
|
+
</stimulus_originator_metadata>
|
|
279
|
+
{available_files}
|
|
280
|
+
|
|
281
|
+
{response_guidelines_prompt}
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def UserStimulusPrompt(
|
|
286
|
+
info: dict, gateway_history: list, errors: list, has_files
|
|
287
|
+
) -> str:
|
|
288
|
+
full_input = info.get("input", {})
|
|
289
|
+
stimulus = full_input.get("originator_input", "")
|
|
290
|
+
current_time = full_input.get("current_time_iso", "")
|
|
291
|
+
|
|
292
|
+
# Determine what (if any) gateway history to include in the prompt
|
|
293
|
+
gateway_history_str = ""
|
|
294
|
+
for item in gateway_history:
|
|
295
|
+
if item.get("role") == "user":
|
|
296
|
+
gateway_history_str += (
|
|
297
|
+
f"\n<{info['tag_prefix']}stimulus>\n"
|
|
298
|
+
f"{item.get('content')}\n"
|
|
299
|
+
f"</{info['tag_prefix']}stimulus>\n"
|
|
300
|
+
)
|
|
301
|
+
elif item.get("role") == "assistant":
|
|
302
|
+
gateway_history_str += (
|
|
303
|
+
f"... Removed orchestrator/assistant processing history ...\n"
|
|
304
|
+
f"<{info['tag_prefix']}stimulus_response>\n"
|
|
305
|
+
f"{item.get('content')}\n"
|
|
306
|
+
f"</{info['tag_prefix']}stimulus_response>\n"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if gateway_history_str:
|
|
310
|
+
gateway_history_str = (
|
|
311
|
+
f"\n<{info['tag_prefix']}stimulus_history>\n"
|
|
312
|
+
f"{gateway_history_str}\n"
|
|
313
|
+
f"</{info['tag_prefix']}stimulus_history>\n"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
query_options = ", ".join(
|
|
317
|
+
[f"`{key}:{value}" for key, value in LLM_QUERY_OPTIONS.items()]
|
|
318
|
+
)
|
|
319
|
+
file_instructions = (
|
|
320
|
+
f"\n<{info['tag_prefix']}file_instructions> Only use the `retrieve_file` action if you absolutely need the content of the file. "
|
|
321
|
+
"Prioritize using the query parameters to get access the content of the file. You don't need to retrieve the file, if you can just return the URL with query parameters. "
|
|
322
|
+
f"Available query parameters are {query_options}."
|
|
323
|
+
"In most cases, you'd only need to return the file block to the gateway.\n\n"
|
|
324
|
+
f"You can return a {FS_PROTOCOL} URL (with optional query parameters) using the format <{info['tag_prefix']}file><url> URL </url></{info['tag_prefix']}file>.\n"
|
|
325
|
+
f"\tExample of returning a file as zip: <{info['tag_prefix']}file><url>{FS_PROTOCOL}://519321d8-3506-4f8d-9377-e5d6ce74d917_filename.csv?encoding=zip</url></{info['tag_prefix']}file>.\n"
|
|
326
|
+
f"You can also optionally return a non-persistent temporary file using the format <{info['tag_prefix']}file name=\"filename.csv\" mime_type=\"text/csv\">\n<data> data </data>\n</{info['tag_prefix']}file>.\n"
|
|
327
|
+
f" can't nest `<{info['tag_prefix']}file>` tags inside the `<data>` tag. If you need to address another file within the data, use the URL with the `resolve=true` query parameter.\n"
|
|
328
|
+
"When using a {FS_PROTOCOL} URL outside of a file block, always include the 'resolve=true' query parameter to ensure the URL is resolved to the actual data.\n"
|
|
329
|
+
f"</{info['tag_prefix']}file_instructions>"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
prompt = (
|
|
333
|
+
"NOTE - this history represents the conversation as seen by the user on the other side of the gateway. It does not include the assistant's invoke actions or reasoning. All of that has been removed, so don't use this history as an example for how the assistant should behave\n"
|
|
334
|
+
f"{gateway_history_str}\n"
|
|
335
|
+
f"<{info['tag_prefix']}stimulus>\n"
|
|
336
|
+
f"{stimulus}\n"
|
|
337
|
+
f"</{info['tag_prefix']}stimulus>\n"
|
|
338
|
+
f"<{info['tag_prefix']}stimulus_metadata>\n"
|
|
339
|
+
f"local_time: {current_time}\n"
|
|
340
|
+
f"</{info['tag_prefix']}stimulus_metadata>\n"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if has_files:
|
|
344
|
+
prompt += file_instructions
|
|
345
|
+
|
|
346
|
+
if len(errors) > 0:
|
|
347
|
+
prompt += (
|
|
348
|
+
f"\n<{info['tag_prefix']}errors>\n"
|
|
349
|
+
"Encountered the following errors while processing the stimulus:\n"
|
|
350
|
+
f"{errors}\n"
|
|
351
|
+
f"</{info['tag_prefix']}errors>\n"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return prompt
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def ActionResponsePrompt(prompt_info: dict) -> str:
|
|
358
|
+
tp = prompt_info["tag_prefix"]
|
|
359
|
+
return (
|
|
360
|
+
f"\n<{tp}action_response>\n"
|
|
361
|
+
f"<{tp}note>This is the agent's response from the assistant's request. The assistant will not thank anyone for these results. Thanking for the results will confuse the originator of the request because they don't see the interaction between the assistant and the agents performing the actions.</{tp}note>\n"
|
|
362
|
+
f"{prompt_info['input']}\n"
|
|
363
|
+
f"</{tp}action_response>\n"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def BasicRagPrompt(
|
|
368
|
+
context_source: str, query: str, context: Dict[str, Any], input_type: str
|
|
369
|
+
) -> str:
|
|
370
|
+
return f"""
|
|
371
|
+
You are doing Retrieval Augmented Generation (RAG) on the {context_source} information to answer the following query:
|
|
372
|
+
<user_query>
|
|
373
|
+
{query}
|
|
374
|
+
</user_query>
|
|
375
|
+
|
|
376
|
+
You will use the RAG model to generate an answer to the query. The answer should be returned with a send_message action.
|
|
377
|
+
Do not do another query as part of the response to this. Either use the context provided or let the originator know that
|
|
378
|
+
there is not enough information to answer the query.
|
|
379
|
+
|
|
380
|
+
The context to use to answer the query is shown below. For any context you use, ensure that there is a link to the
|
|
381
|
+
source of the context. The link must be in the appropriate format slack or web the web format is: '[' <link-text> '](' <url> ']' and the slack format is: '<' <url> '|' <link-text> '>'. This message should be formatted in the {input_type} format.
|
|
382
|
+
Be very careful about your answer since it will be used in a professional setting and their
|
|
383
|
+
reputation is on the line.
|
|
384
|
+
|
|
385
|
+
<context_yaml>
|
|
386
|
+
{yaml.dump(context)}
|
|
387
|
+
</context_yaml>
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def ContextQueryPrompt(query: str, context: str) -> HumanMessage:
|
|
392
|
+
return f"""
|
|
393
|
+
You (orchestrator) are being asked to query, comment on or edit the following text following the originator's request.
|
|
394
|
+
Do your best to give a complete and accurate answer using only the context given below. Ensure that you
|
|
395
|
+
include links to the source of the information.
|
|
396
|
+
|
|
397
|
+
Do not make up information. If the context does not include the information you need just say that you
|
|
398
|
+
can't answer the question. Try your best - it is very important that you give the best answer possible.
|
|
399
|
+
|
|
400
|
+
<user_request>
|
|
401
|
+
{query}
|
|
402
|
+
</user_request>
|
|
403
|
+
|
|
404
|
+
The context to use to answer the query is shown below. If the context in not enough to satisfy the request,
|
|
405
|
+
you should ask the originator for more information. Include links to the source of the data.
|
|
406
|
+
|
|
407
|
+
<context>
|
|
408
|
+
{context}
|
|
409
|
+
</context>
|
|
410
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from ....common.constants import DEFAULT_IDENTITY_KEY_FIELD
|
|
5
|
+
|
|
6
|
+
class BaseAuthorizationProvider(ABC):
|
|
7
|
+
PERMISSIVE_SCOPES = ["*:*:*"]
|
|
8
|
+
NO_SCOPES = []
|
|
9
|
+
|
|
10
|
+
def __init__(self, config: dict):
|
|
11
|
+
self.base_roles = config.get("base_roles", ["least_privileged_user"])
|
|
12
|
+
self.role_to_scope_mappings = config.get(
|
|
13
|
+
"role_to_scope_mappings",
|
|
14
|
+
{"least_privileged_user": BaseAuthorizationProvider.NO_SCOPES},
|
|
15
|
+
)
|
|
16
|
+
self.configuration = config.get("configuration", {})
|
|
17
|
+
self.authorization_field = config.get("key_field", DEFAULT_IDENTITY_KEY_FIELD)
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_authorization_field(self) -> str:
|
|
21
|
+
"""Returns the configured field name to use for authorization lookup"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_scopes(self, identity: str) -> List[str]:
|
|
26
|
+
"""Get scopes for a given identity."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_roles(self, identity: str) -> List[str]:
|
|
31
|
+
"""Get roles for a given identity."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def is_authorized(originator_scopes: List[str], agent_scopes: List[str]) -> bool:
|
|
36
|
+
"""Check if the originator's scopes authorize access to the agent's scopes."""
|
|
37
|
+
return any(
|
|
38
|
+
BaseAuthorizationProvider._scope_matches(originator_scope, agent_scope)
|
|
39
|
+
for originator_scope in originator_scopes
|
|
40
|
+
for agent_scope in agent_scopes
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _scope_matches(originator_scope: str, agent_scope: str) -> bool:
|
|
45
|
+
if not isinstance(originator_scope, str) or not isinstance(agent_scope, str):
|
|
46
|
+
return False
|
|
47
|
+
if not originator_scope or not agent_scope:
|
|
48
|
+
return False
|
|
49
|
+
originator_parts = originator_scope.split(":")
|
|
50
|
+
agent_parts = agent_scope.split(":")
|
|
51
|
+
if len(originator_parts) != 3 or len(agent_parts) != 3:
|
|
52
|
+
return False
|
|
53
|
+
return all(
|
|
54
|
+
orig == agent or orig == "*" or agent == "*"
|
|
55
|
+
for orig, agent in zip(originator_parts, agent_parts)
|
|
56
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""A module to interact with BambooHR API and hold employee data"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import datetime
|
|
5
|
+
import base64
|
|
6
|
+
import requests
|
|
7
|
+
from ...common.utils import parse_yaml_response
|
|
8
|
+
from ...common.prompt_templates import (
|
|
9
|
+
FilterLocationsSystemPrompt,
|
|
10
|
+
FilterLocationsUserPrompt,
|
|
11
|
+
)
|
|
12
|
+
from ..common.singleton import SingletonMeta
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BambooHR(metaclass=SingletonMeta):
|
|
16
|
+
"""A service to interact with BambooHR API and hold employee data"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: dict = {}) -> None:
|
|
19
|
+
self._api_key = config.get("bamboo_hr_api_key", os.environ.get("BAMBOO_HR_API_KEY"))
|
|
20
|
+
self._base_url = config.get("bamboo_hr_base_url", os.environ.get("BAMBOO_HR_BASE_URL"))
|
|
21
|
+
self._employee_data = None
|
|
22
|
+
self._last_fetch = None
|
|
23
|
+
|
|
24
|
+
if not self._api_key:
|
|
25
|
+
raise ValueError("BambooHR API key not set, please set BAMBOO_HR_API_KEY environment variable")
|
|
26
|
+
if not self._base_url:
|
|
27
|
+
raise ValueError("BambooHR base URL not set, please set BAMBOO_HR_BASE_URL environment variable")
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def employee_data(self) -> dict:
|
|
31
|
+
if not self._employee_data:
|
|
32
|
+
self._employee_data = self.fetch_employee_data()
|
|
33
|
+
elif (datetime.datetime.now() - self._last_fetch).total_seconds() > 12 * 3600:
|
|
34
|
+
# If last fetch was more than 12 hours ago, fetch again
|
|
35
|
+
self._employee_data = self.fetch_employee_data()
|
|
36
|
+
return self._employee_data
|
|
37
|
+
|
|
38
|
+
def get_employee(self, email: str) -> dict:
|
|
39
|
+
return self.employee_data.get(email.lower())
|
|
40
|
+
|
|
41
|
+
def get_employee_summary(self, email: str) -> dict:
|
|
42
|
+
employee = self.get_employee(email)
|
|
43
|
+
if not employee:
|
|
44
|
+
return None
|
|
45
|
+
return {
|
|
46
|
+
"name": employee.get("displayName"),
|
|
47
|
+
"title": employee.get("jobTitle"),
|
|
48
|
+
"email": employee.get("workEmail"),
|
|
49
|
+
"phone": employee.get("workPhone"),
|
|
50
|
+
"mobile": employee.get("mobilePhone"),
|
|
51
|
+
"department": employee.get("department"),
|
|
52
|
+
"location": employee.get("location"),
|
|
53
|
+
"supervisor": employee.get("supervisor"),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def get_away_info(self, email: str, days_away: int = 30) -> str:
|
|
57
|
+
now = datetime.datetime.now()
|
|
58
|
+
start = datetime.datetime.strftime(now, "%Y-%m-%d")
|
|
59
|
+
end = datetime.datetime.strftime(now + datetime.timedelta(days=days_away), "%Y-%m-%d")
|
|
60
|
+
|
|
61
|
+
url = os.path.join(self._base_url, "time_off/whos_out")
|
|
62
|
+
response = self.api_request(url, {"start": start, "end": end})
|
|
63
|
+
away_info = {}
|
|
64
|
+
for away_employee in response:
|
|
65
|
+
eid = str(away_employee.get("employeeId"))
|
|
66
|
+
if eid not in away_info:
|
|
67
|
+
away_info[eid] = []
|
|
68
|
+
away_info[eid].append(
|
|
69
|
+
{
|
|
70
|
+
"name": away_employee.get("name"),
|
|
71
|
+
"start": away_employee.get("start"),
|
|
72
|
+
"end": away_employee.get("end"),
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
employee = self.get_employee(email)
|
|
76
|
+
if not employee:
|
|
77
|
+
return None
|
|
78
|
+
employee_id = employee.get("id")
|
|
79
|
+
employee_away_info = away_info.get(employee_id, [])
|
|
80
|
+
return {
|
|
81
|
+
"employee": employee.get("displayName"),
|
|
82
|
+
"away_info": employee_away_info,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def get_reports(self, email: str, levels: int = -1) -> list:
|
|
86
|
+
employee = self.get_employee(email)
|
|
87
|
+
if not employee:
|
|
88
|
+
return None
|
|
89
|
+
reports = []
|
|
90
|
+
self.get_reports_recursive(employee, reports, levels)
|
|
91
|
+
return reports
|
|
92
|
+
|
|
93
|
+
def get_all_employees(self) -> list:
|
|
94
|
+
return list(self.employee_data.values())
|
|
95
|
+
|
|
96
|
+
def get_reports_recursive(self, employee, reports, levels):
|
|
97
|
+
if levels == 0:
|
|
98
|
+
return
|
|
99
|
+
if employee.get("reports"):
|
|
100
|
+
for report in employee.get("reports"):
|
|
101
|
+
reports.append(report)
|
|
102
|
+
self.get_reports_recursive(report, reports, levels - 1)
|
|
103
|
+
|
|
104
|
+
def get_list_of_all_locations(self) -> list:
|
|
105
|
+
# Go through all employees and get the location
|
|
106
|
+
locations = set()
|
|
107
|
+
for employee in self.employee_data.values():
|
|
108
|
+
location = employee.get("location")
|
|
109
|
+
if location:
|
|
110
|
+
locations.add(location)
|
|
111
|
+
return sorted(list(locations))
|
|
112
|
+
|
|
113
|
+
def get_employees_by_location(self, location: str, llm_service_fn: callable) -> list:
|
|
114
|
+
# We are going to use an LLM to decide if Bamboo locations should
|
|
115
|
+
# be included in the list of locations
|
|
116
|
+
if not llm_service_fn:
|
|
117
|
+
raise ValueError("LLM service function not set in BambooHR instance")
|
|
118
|
+
response = llm_service_fn(
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
"role": "system",
|
|
122
|
+
"content": FilterLocationsSystemPrompt(self.get_list_of_all_locations()),
|
|
123
|
+
},
|
|
124
|
+
{"role": "user", "content": FilterLocationsUserPrompt(location)},
|
|
125
|
+
]
|
|
126
|
+
).get("content")
|
|
127
|
+
|
|
128
|
+
response = parse_yaml_response(response)
|
|
129
|
+
employees = []
|
|
130
|
+
if response and isinstance(response, dict) and response.get("matching_locations"):
|
|
131
|
+
location_set = set(response["matching_locations"])
|
|
132
|
+
for employee in self.employee_data.values():
|
|
133
|
+
if employee.get("location") in location_set:
|
|
134
|
+
employees.append(employee)
|
|
135
|
+
return employees
|
|
136
|
+
|
|
137
|
+
def fetch_employee_data(self) -> dict:
|
|
138
|
+
# First fetch the directory report from Bamboo
|
|
139
|
+
url = os.path.join(self._base_url, "employees/directory")
|
|
140
|
+
response = self.api_request(url)
|
|
141
|
+
employee_data = self.build_org_chart(response)
|
|
142
|
+
|
|
143
|
+
self._last_fetch = datetime.datetime.now()
|
|
144
|
+
self._employee_data = employee_data
|
|
145
|
+
return employee_data
|
|
146
|
+
|
|
147
|
+
def api_request(self, url: str, params: dict = None) -> dict:
|
|
148
|
+
basic_auth = self.encode_basic_auth(self._api_key, "x")
|
|
149
|
+
headers = {
|
|
150
|
+
"Authorization": f"Basic {basic_auth}",
|
|
151
|
+
"Accept": "application/json",
|
|
152
|
+
}
|
|
153
|
+
response = requests.get(url, headers=headers, params=params, timeout=10)
|
|
154
|
+
if response.status_code != 200:
|
|
155
|
+
raise ValueError(f"Failed to fetch data: {response.status_code}")
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
def build_org_chart(self, directory_report) -> dict:
|
|
159
|
+
employee_data = {}
|
|
160
|
+
by_name = {}
|
|
161
|
+
for employee in directory_report.get("employees", []):
|
|
162
|
+
email = employee.get("workEmail")
|
|
163
|
+
if not email:
|
|
164
|
+
continue
|
|
165
|
+
employee_data[email.lower()] = employee
|
|
166
|
+
by_name[employee["displayName"]] = employee
|
|
167
|
+
|
|
168
|
+
for employee in directory_report.get("employees", []):
|
|
169
|
+
manager_name = employee.get("supervisor")
|
|
170
|
+
if manager_name:
|
|
171
|
+
manager = by_name.get(manager_name)
|
|
172
|
+
if manager:
|
|
173
|
+
manager["reports"] = manager.get("reports", [])
|
|
174
|
+
manager["reports"].append(employee)
|
|
175
|
+
employee["manager"] = manager
|
|
176
|
+
return employee_data
|
|
177
|
+
|
|
178
|
+
def encode_basic_auth(self, username: str, password: str) -> str:
|
|
179
|
+
user_pass = f"{username}:{password}"
|
|
180
|
+
encoded_bytes = base64.b64encode(user_pass.encode("utf-8"))
|
|
181
|
+
encoded_str = str(encoded_bytes, "utf-8")
|
|
182
|
+
return encoded_str
|