# Valispace Script for Custom Actions # Purpose: Assess requirement text quality based on INCOSE criteria # Developed by [Paul Grey] # Date: [Date] #IMPORTANT: Provide your valispace url in the Configuration Section below from typing import Any, Dict from valispace import API import ast # --------------------------------------------- # Configuration Section VALISPACE = { "domain": "https://YOUR_DEPLOYMENT_NAME.valispace.com", # Base URL for the Valispace instance "warn_https": False, # Disable HTTPS warnings if set to False } # --------------------------------------------- def initialize_api(temporary_access_token) -> API: """ Initialize the Valispace API. Returns: API: Initialized Valispace API object. """ return API( url=VALISPACE["domain"], session_token=temporary_access_token, warn_https=VALISPACE.get("warn_https", False) ) def get_custom_field_dict(custom_field_rows, custom_fields, field_name, project_id, api): #print("Costum Field Dict Function Starts") """ Retrieve custom field ID and custom field dictionary for a specified field name. Args: custom_field_rows: List of custom field rows. custom_fields: List of custom fields. field_name: Name of the custom field. Returns: Tuple: (custom_field_dict, custom_field_id) """ custom_field = None for cf in custom_fields: if cf["name"].lower() == field_name.lower(): custom_field_header = cf["name"] custom_field = cf if custom_field == None: response = api.request('POST', 'data/custom-fields/', {"projects": [project_id],"target_content_type": 120,"name": file_name,"type": 0}) custom_field_header = field_name custom_field = response else: projects = custom_field["projects"] projects.append(project_id) response = api.request('PATCH', 'data/custom-fields/' + str(custom_field["id"]) + '/', {"projects": projects}) custom_field_dict = {} for custom_field_row in custom_field_rows: if custom_field_row["custom_field"] == custom_field["id"]: custom_field_dict[custom_field_row["referenced_object_id"]] = { "custom_field_header": custom_field_header, "custom_field_id": custom_field_row["custom_field"], "custom_field_row_id": custom_field_row["id"], "text": custom_field_row["value"] } return custom_field_dict, custom_field["id"] def add_tag_to_requirement(api, requirement_id, tag_name, project_id): """ Add a tag to a requirement based on the quality check result. Args: api: Initialized Valispace API object. requirement_id: ID of the requirement. tag_name: Name of the tag to be added. """ # Retrieve current tags requirement = api.request("GET", f"requirements/{requirement_id}/") current_tags = requirement.get("tags", []) # Retrieve all tags to find the ID of the tag to be added tags = api.request('GET', f'tags/?project={project_id}') tag_id = None for tag in tags: if tag['name'] == tag_name: tag_id = tag['id'] break # If the tag doesn't exist, create it if not tag_id: if tag_name == "ValiAssistant Quality Check Failed!": tag_data = { "name": tag_name, "color": "#000000", "project": project_id } new_tag = api.request('POST', 'tags/', tag_data) tag_id = new_tag['id'] elif tag_name == "ValiAssistant Quality Check Passed!": tag_data = { "name": tag_name, "color": "#00FF2F", "project": project_id } new_tag = api.request('POST', 'tags/', tag_data) tag_id = new_tag['id'] if tag_id not in current_tags: current_tags.append(tag_id) api.request('POST', 'requirements/bulk-add-values/', {"ids": [requirement_id], "field": "tags", "values": [tag_id]}) print(f"Tag '{tag_name}' added to requirement {requirement_id}") def remove_tag_from_requirement(api, requirement_id, tag_name, project_id): # Retrieve current tags requirement = api.request("GET", f"requirements/{requirement_id}/") current_tags = requirement.get("tags", []) # Retrieve all tags to find the ID of the tag to be removed tags = api.request('GET', f'tags/?project={project_id}') tag_id = None for tag in tags: if tag['name'] == tag_name: tag_id = tag['id'] break # Remove tag if present if tag_id in current_tags: api.request('POST', 'requirements/bulk-remove-values/', {"ids": [requirement_id], "field": "tags", "values": [tag_id]}) return 0 else: print(f"No tag {tag_name} present on Requirement {requirement_id}") return 0 def write_data_to_custom_column(api, req_id, text, custom_field_dict, custom_field_id): #print("Data to Custom Column Function Starts") """ Write data to the custom column for a specific requirement. Args: api: Initialized Valispace API object. req_id: ID of the requirement. text: Text to write. custom_field_dict: Dictionary of custom fields. custom_field_id: ID of the custom field. """ if text == "": return if req_id in custom_field_dict: if text != custom_field_dict[req_id]["text"]: api.request("POST", "data/custom-field-row/bulk-update/", { custom_field_dict[req_id]["custom_field_row_id"]: {"value": text} }) print(f"Custom Field Row updated for {req_id}") else: api.request("POST", "data/custom-field-row/bulk-create/?save_history=true/", [{ "custom_field": custom_field_id, "referenced_object_id": req_id, "value": text }]) print(f"Custom Field Row created for {req_id}") def main(**kwargs) -> Dict[str, Any]: #print("Main Function Starts") """ Main function to execute the script. Args: **kwargs: Additional arguments passed to the script. Returns: Dict[str, Any]: Result data to be sent back to Valispace. """ api = initialize_api(kwargs['temporary_access_token']) objects_ids = kwargs.get("objects_ids", []) project_id = api.request('GET', f'requirements/{objects_ids[0]}')['project'] print(project_id) # Tags for Quality Check passed_tag = "ValiAssistant Quality Check Passed!" failed_tag = "ValiAssistant Quality Check Failed!" # Retrieve custom field and custom field rows data custom_fields = api.get(f"data/custom-fields/") custom_field_rows = api.get(f"data/custom-field-row/?project={project_id}") quality_comments_dict, quality_comments_id = get_custom_field_dict(custom_field_rows, custom_fields, "Requirement Text Quality Comments", project_id, api) for obj_id in objects_ids: # Define the custom prompt custom_prompt = """ Consider the following INCOSE Standards for writing Requirements: R1 - Use a structured, complete sentence: subject, verb, object. R2 - Use the active voice in the main sentence structure of the need or requirement statement with the responsible entity clearly identified as the subject of the sentence. R3 - Ensure the subject and verb of the need or requirement statement are appropriate to the entity to which the need or requirement refers. R5 - Use definite article “the” rather than the indefinite article “a”. R6 - Use appropriate units when stating quantities. All numbers should have units of measure explicitly stated. R7 - Avoid the use of vague terms such as “some”, “any”, “allowable”, “several”, “many”, “a lot of”, “a few”, “almost always”, “very nearly”, “nearly”, “about”, “close to”, “almost”, and “approximate”. R8 - Avoid escape clauses such as “so far as is possible”, “as little as possible”, “where possible”, “as much as possible”, “if it should prove necessary”, “if necessary”, “to the extent necessary”, “as appropriate”, “as required”, “to the extent practical”, and “if practicable”. R9- Avoid open-ended clauses such as “including but not limited to”, “etc.” and “and so on”. R10 - Avoid superfluous infinitives such as “be designed to”, “be able to”, “be capable of”. R12, 13, 14 - Use correct grammar, spelling, punctuation. R15 - Use a defined convention to express logical expressions such as “[X AND Y]”, “[X OR Y]”, [X XOR Y]”, “NOT[X OR Y]”. R16 - Avoid the use of “not” R17 - Avoid the use of the oblique ("/") symbol except in units, i.e. km/hr R18 - Write a single sentence that contains a single thought conditioned and qualified by relevant sub-clauses. R19 - Avoid combinators that join clauses, such as “and”, “or”, ”then”, ”unless”, ”but”, ”as well as”, ”but also”, ”however”, ”whether”, ”meanwhile”, ”whereas”, ”on the other hand”, or ”otherwise”. R20 - Avoid phrases that indicate the purpose of the need or requirement. R21 - Avoid parentheses and brackets containing subordinate text. R22 - Enumerate sets explicitly instead of using a group noun to name the set. R24 - Avoid the use of pronouns and indefinite pronouns. R26 - Avoid using unachievable absolutes such as 100'%' reliability, 100'%' availability, all, every, always, never, etc. R28 - Express the propositional nature of a condition explicitly for a single action instead of giving lists of actions for a specific condition. R29 - Classify the needs and requirements according to the aspects of the problem or system it addresses. R31 - When defining design inputs avoid stating a solution unless there is rationale for constraining the design. Focus on the problem “what” rather than the solution “how”. R32 - Use “each” instead of “all”, “any" or “both” when universal quantification is intended. R33 - Define quantities with a range of values appropriate to the entity to which they apply and to which the entity will be verified or validated against. R34 - Provide specific measurable performance targets appropriate to the entity to which the need or requirement is stated and against which the entity will be verified to meet. R35 - Define temporal dependencies explicitly instead of using indefinite temporal keywords such as “eventually”, “until”, “before”, “after”, “as”, “once”, “earliest”, “latest”, “instantaneous”, “simultaneous”, “at last”. R38 - Avoid the use of abbreviations. Based on these INCOSE standards for Requirements ONLY return me a dictionary with the quality_score_of_the_requirement_text from 0 to 100 and a corresponding comment_for_improvement in the form of: {'Score': quality_score_of_the_requirement_text, 'Comment': comment_for_improvement}. """ #{'Score': integer of a quality score between 0 and 100 of the Requirement Text, {'Improvement': String of imrpovement suggestion from the list above}} # Send the prompt to the assistant print("Prompt starts") assistant_response = api.general_prompt( custom_prompt=custom_prompt, model="requirement", field="text", objects_ids=[obj_id], parallel=True, replace_valis_by_id=False ) print(assistant_response) if 'errors' not in assistant_response or assistant_response['errors'] == ['']: results = assistant_response.get('results', {}).get(str(obj_id), "") print(results) overall_score = int(results["Score"]) improvement_suggestion = results ["Comment"] # Determine the tag based on score and add it if overall_score <= 80: remove_tag_from_requirement(api, obj_id, passed_tag, project_id) add_tag_to_requirement(api, obj_id, failed_tag, project_id) if improvement_suggestion: write_data_to_custom_column(api, obj_id, improvement_suggestion, quality_comments_dict, quality_comments_id) else: # Remove any previous Quality Information remove_tag_from_requirement(api, obj_id, failed_tag, project_id) write_data_to_custom_column(api, obj_id, improvement_suggestion, quality_comments_dict, quality_comments_id) # Add Quality Pass Tag to Req add_tag_to_requirement(api, obj_id, passed_tag, project_id) else: error_message = "Errors encountered in the assistant response for object ID {}: {}".format( obj_id, ", ".join(assistant_response.get('errors'))) print(error_message) return { "result": "Quality assessment completed." } if __name__=='__main__': main()