Repository: RasaHQ/rasa
Pull request: Fix API docs
**Proposed changes**:

Fixes https://github.com/RasaHQ/rasa/issues/10148

- Updates `/model/train` endpoint in docs to only accept YAML input
- Updates `/model` endpoint to clarify model path

Problem statement:
Model API QA: Errors met when testing model training and replacing loaded model
**Problem description**:

1. Testing `/model/train` endpoint with the example in the docs leads to a YAML validation error (400 Bad Request error:
```
"The request body does not contain valid YAML. Error: Failed to validate YAML. Please make sure the file is correct and all mandatory parameters are specified. Here are the errors found during validation:\n  in Line 1:\n      Value 'b'- intent: greet\\\\n examples: |\\\\n - hey\\\\n - hello\\\\n - hi'' is not a list. Value path: '/nlu'"
```

2. Testing PUT `/model` endpoint with this request body (existing file on disk) leads to 400 Bad Request:

```  
{
"model_file": "/models/20211110-091805-impulsive-birth.tar.gz"
}
```
It seems not to be able to identify the path given, this is the response error message:
`"Agent with name '/models/20211110-091805-impulsive-birth.tar.gz' could not be loaded."`
The debug log shows this user warning:
`UserWarning: No valid configuration given to load agent. Agent loaded with no model!`

Hints from issue discussion:
Found the same - @tayfun I see you're assigned so maybe you already saw this, but it expects literal `yaml`, so a documentation issue - this request works:
```
curl --request POST \
  --url http://localhost:5005/model/train \
  --header 'Content-Type: text/yaml' \
  --data 'version: '\''3.0'\''
config:
  store_entities_as_slots: true
session_config:
  session_expiration_time: 60
  carry_over_slots_to_new_session: true
intents:
- greet:
    use_entities: true
- goodbye:
    use_entities: true
- affirm:
    use_entities: true
- deny:
    use_entities: true
- mood_great:
    use_entities: true
- mood_unhappy:
    use_entities: true
- bot_challenge:
    use_entities: true
entities:
- name
slots:
  name:
    type: rasa.shared.core.slots.AnySlot
    initial_value: null
    influence_conversation: false
    mappings:
    - type: from_entity
      entity: name
  yes:
    type: rasa.shared.core.slots.AnySlot
    initial_value: null
    influence_conversation: false
    mappings:
    - type: from_intent
      intent: affirm
      value: yes
    - type: from_intent
      not_intent: affirm
      value: no
responses:
  utter_greet:
  - text: Hey {name}! How are you?
  utter_cheer_up:
  - text: '\''Here is something to cheer you up:'\''
    image: https://i.imgur.com/nGF1K8f.jpg
  utter_did_that_help:
  - text: Did that help you?
  utter_happy:
  - text: Great, carry on!
  utter_goodbye:
  - text: Bye
  utter_iamabot:
  - text: I am a bot, powered by Rasa.
actions: []
forms: {}
e2e_actions: []
nlu:
- intent: greet
  examples: |
    - hey [Molly](name)
    - hello [Bob](name)
    - hi [Chris](name)
    - hello there [Jim](name)
    - good morning [Fred](name)
    - good evening [Sally](name)
    - moin
    - hey there
    - let'\''s go
    - hey dude
    - goodmorning
    - goodevening
    - good afternoon
    - hi
- intent: goodbye
  examples: |
    - cu
    - good by
    - cee you later
    - good night
    - bye
    - goodbye
    - have a nice day
    - see you around
    - bye bye
    - see you later
stories:
- story: happy path
  steps:
  - intent: greet
  - action: utter_greet
  - intent: mood_great
  - action: utter_happy
rules:
- rule: Say goodbye anytime the user says goodbye
  steps:
  - intent: goodbye
  - action: utter_goodbye
- rule: Say '\''I am a bot'\'' anytime the user challenges
  steps:
  - intent: bot_challenge
  - action: utter_iamabot
'
```
@ancalita I think the model replacement 400s because of the absolute path - was the model really at the root of the machine?  

If I follow the doc and use the full path to an existing model, instead of a 400 I get a 204, but the loaded model's name doesn't change:
```
 $ curl --request PUT \
  --url http://localhost:5005/model \
  --header 'Content-Type: application/json' \
  --data '{
  "model_file": "/Users/melinda/rasa_bots/3.0rc2_QA/models/20211115-213850-covering-bosun.tar.gz"
}' -vv
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 5005 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5005 (#0)
> PUT /model HTTP/1.1
> Host: localhost:5005
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 101
> 
* upload completely sent off: 101 out of 101 bytes
< HTTP/1.1 204 No Content
< Access-Control-Expose-Headers: filename
< Access-Control-Allow-Credentials: true
< connection: keep-alive
< 
* Connection #0 to host localhost left intact
* Closing connection 0
```

status remains the same as before:
```
$ curl --request GET \
  --url 'http://localhost:5005/status'
  
{
  "model_file": "20211115-220201-witty-agent.tar.gz",
  "model_id": "b0d7f6d363ff47cba922c33dbe38fb1b",
  "num_active_training_jobs": 0
}
```

 there's only a log about `UnexpectTED` in the logs to show it read the model at all
```
2021-11-15 22:12:23 WARNING  rasa.shared.utils.common  - The UnexpecTED Intent Policy is currently experimental and might change or be removed in the future 🔬 Please share your feedback on it in the forum (https://forum.rasa.com) to help us make this feature ready for production.
```

Also: After unloading the model with a `DELETE`, doing the `PUT` just reactivates the previously loaded model. 
> @ancalita I think the model replacement 400s because of the absolute path - was the model really at the root of the machine?

@melindaloubser1 Oh that could have been the problem, not using absolute path, because indeed the models were not really at the root directory. But probably this could be made clearer in the docs (to use absolute paths) for those naive folks like myself copy-pasting examples 😅 


Yes, yes you are both right @melindaloubser1 @ancalita . I need to update the documentation (and remove obsolete code which was handling JSON since only YAML is supported now). I'm struggling with redocly, specifically only using yaml in documentation has not been easy. 
^^ I've had a fight with the API spec and YAML example snippets too, really cannot figure out what the magic is there besides that specifying the ref to the snippet separately for both schema & example sort-of works

Commit message: Merge branch 'main' into tayfun/10148-fix-api-docs

Patch to review:
diff --git a/docs/static/spec/rasa.yml b/docs/static/spec/rasa.yml
index 1a2029b90e00..5ee7be468259 100644
--- a/docs/static/spec/rasa.yml
+++ b/docs/static/spec/rasa.yml
@@ -482,12 +482,12 @@ paths:
         description: >-
           The training data should be in YAML format.
         content:
-          application/json:
-            schema:
-              $ref: '#/components/schemas/JSONTrainingRequest'
-          application/x-yaml:
+          application/yaml:
             schema:
-              type: string
+              $ref: '#/components/schemas/YAMLTrainingRequest'
+            example:
+              $ref: '#/components/examples/YAMLTrainingRequest'
+
 
       responses:
         200:
@@ -718,7 +718,8 @@ paths:
       summary: Replace the currently loaded model
       description: >-
         Updates the currently loaded model.
-        First, tries to load the model from the local storage system.
+        First, tries to load the model from the local (note: local to Rasa server)
+        storage system.
         Secondly, tries to load the model from the provided model server configuration.
         Last, tries to load the model from the provided remote storage.
       requestBody:
@@ -1052,7 +1053,7 @@ components:
         model_file:
           type: string
           description: Path to model file
-          example: "/models/20190512.tar.gz"
+          example: "/CHANGE-TO-DIR-ON-REMOTE-SERVER/models/20190512.tar.gz"
         model_server:
           $ref: '#/components/schemas/EndpointConfig'
         remote_storage:
@@ -1407,19 +1408,40 @@ components:
           type: integer
           description: Time to wait between pulls from model server
 
-    JSONTrainingRequest:
+    YAMLTrainingRequest:
       type: object
       properties:
-        domain:
-          $ref: '#/components/schemas/DomainFile'
-        config:
-          $ref: '#/components/schemas/ConfigFile'
+        intents:
+          type: array
+          description: All intent names and properties
+          items:
+            $ref: '#/components/schemas/IntentDescription'
+        responses:
+          description: Bot response templates
+          type: object
+          additionalProperties:
+            $ref: '#/components/schemas/TemplateDescription'
+        session_config:
+          description: Session configuration options
+          type: object
+          properties:
+            session_expiration_time:
+              type: integer
+            carry_over_slots_to_new_session:
+              type: boolean
+        pipeline:
+          description: Pipeline list
+          type: array
+        policies:
+          description: Policies list
         nlu:
           $ref: '#/components/schemas/NLUTrainingData'
-        responses:
-          $ref: '#/components/schemas/RetrievalIntentsTrainingData'
+        rules:
+          description: Rule list
+          type: array
         stories:
           $ref: '#/components/schemas/StoriesTrainingData'
+
         force:
           type: boolean
           description: >-
@@ -1432,7 +1454,6 @@ components:
             If `true` (default) the trained model will be saved in the default model
             directory, if `false` it will be saved in a temporary directory
           deprecated: True
-      required: ["config"]
 
     NLUTrainingData:
       type: string
@@ -1860,3 +1881,50 @@ components:
           type: string
           description: Text of the message
           example: "Hello!"
+  examples:
+    YAMLTrainingRequest: |
+      intents:
+        - greet
+        - goodbye
+
+      responses:
+        utter_greet:
+        - text: "Hey! How are you?"
+
+        utter_goodbye:
+        - text: "Bye"
+
+      session_config:
+        session_expiration_time: 60
+        carry_over_slots_to_new_session: true
+
+      pipeline:
+
+      policies:
+
+      nlu:
+      - intent: greet
+        examples: |
+          - hey
+          - hello
+
+      - intent: goodbye
+        examples: |
+          - bye
+          - goodbye
+
+      rules:
+
+      - rule: Say goodbye anytime the user says goodbye
+        steps:
+        - intent: goodbye
+        - action: utter_goodbye
+
+      stories:
+
+      - story: happy path
+        steps:
+        - intent: greet
+        - action: utter_greet
+        - intent: goodbye
+        - action: utter_goodbye
diff --git a/rasa/server.py b/rasa/server.py
index 5a46e9b0e217..ba0b00514b9b 100644
--- a/rasa/server.py
+++ b/rasa/server.py
@@ -1422,34 +1422,6 @@ def _test_data_file_from_payload(request: Request, temporary_directory: Path) ->
     )
 
 
-def _validate_json_training_payload(rjs: Dict) -> None:
-    if "config" not in rjs:
-        raise ErrorResponse(
-            HTTPStatus.BAD_REQUEST,
-            "BadRequest",
-            "The training request is missing the required key `config`.",
-            {"parameter": "config", "in": "body"},
-        )
-
-    if "nlu" not in rjs and "stories" not in rjs:
-        raise ErrorResponse(
-            HTTPStatus.BAD_REQUEST,
-            "BadRequest",
-            "To train a Rasa model you need to specify at least one type of "
-            "training data. Add `nlu` and/or `stories` to the request.",
-            {"parameters": ["nlu", "stories"], "in": "body"},
-        )
-
-    if "stories" in rjs and "domain" not in rjs:
-        raise ErrorResponse(
-            HTTPStatus.BAD_REQUEST,
-            "BadRequest",
-            "To train a Rasa model with story training data, you also need to "
-            "specify the `domain`.",
-            {"parameter": "domain", "in": "body"},
-        )
-
-
 def _training_payload_from_yaml(
     request: Request, temp_dir: Path, file_name: Text = "data.yml"
 ) -> Dict[Text, Any]:


Code review comments:
diff_hunk: @@ -1407,19 +1408,40 @@ components:
           type: integer
           description: Time to wait between pulls from model server
 
-    JSONTrainingRequest:
+    YAMLTrainingRequest:
       type: object
       properties:
-        domain:
-          $ref: '#/components/schemas/DomainFile'
-        config:
-          $ref: '#/components/schemas/ConfigFile'
+        intents:
path: docs/static/spec/rasa.yml
text: @user1:
This section should include all properties (missing ones from the domain include actions, entities, e2e_actions, config) and be ordered (IMO) by config.yml, then domain.yml, then the data files

@author:
> This section should include all properties (missing ones from the domain include actions, entities, e2e_actions, config) and be ordered (IMO) by config.yml, then domain.yml, then the data files

@user2 are you sure actions etc. would be handled here? I don't see it in stale docs either: https://rasa.com/docs/rasa/pages/http-api#operation/trainModel I've taken the example and schema from unit tests.

@user3:
Isn't all of this covered by `#/components/schemas/DomainFile`?

@author:
@user3 It's not under `domain` anymore, request body is flattened. This is why I've newly added this schema. Also, examples inside schemas do not work for any content type except for JSON in redocly, so `example` keys were removed and a separate example on the same level as schema was added. The other thing is I've simplified the example, previously there were 6 intents in the example request, I've added 2 to show how a list can be sent, I think people can extrapolate that and add more intents if needed.

diff_hunk: @@ -1860,3 +1881,50 @@ components:
           type: string
           description: Text of the message
           example: "Hello!"
+  examples:
+    YAMLTrainingRequest: |
+      intents:
+        - greet
+        - goodbye
+
+      responses:
+        utter_greet:
+        - text: "Hey! How are you?"
path: docs/static/spec/rasa.yml
text: @user1:
Same on this section: reorder by config.yml then domain.yml then data, and add all keys even if they're empty e.g. `actions: []`

@author:
> Same on this section: reorder by config.yml then domain.yml then data, and add all keys even if they're empty e.g. `actions: []`

@user2 I don't think actions are used here. I don't understand the ordering, why would it `session_config` come before others? Or do you mean `pipeline` comes first, then `policies`?

@user1:
For this particular example, there are no custom actions to pass, so you are correct. However, in a bot with custom actions, or slots, or entities, these sections would need to be included.

Re. the ordering: Sorry for the shorthand, I meant the contents of config.yml, so yes, pipeline + policies, then domain content (intents, entities, actions, e2e_actions, forms, slots, responses, config<- this is a key in the domain in 3.0, which is quite confusing) then content of nlu, rules and stories. It's not required to do it this way for the example to work, but will make it much easier for readers to understand the example

@author:
@user2 Yeah I meant do you know custom actions are used if provided in request body like here? I don't see that in code, and I don't see them being tested in unit tests. I wouldn't be surprised if not all CLI options were supported here. 

@user1:
They would have to be supported to train on stories which include custom actions I believe

diff_hunk: @@ -1052,7 +1053,7 @@ components:
         model_file:
           type: string
           description: Path to model file
-          example: "/models/20190512.tar.gz"
+          example: "/CHANGE-TO-DIR-ON-REMOTE-SERVER/models/20190512.tar.gz"
path: docs/static/spec/rasa.yml
text: @user1:
```suggestion
          example: "/absolute-path-to-models-directory/models/20190512.tar.gz"
```