We use essential cookies for the website to function, as well as analytics cookies for analyzing and creating statistics of the website performance. To agree to the use of analytics cookies, click "Accept All". You can manage your preferences at any time by clicking "Cookie Settings" on the footer. More Information.

Only Essential Cookies
Accept All

Complete Sample Code

NOTE

You need to apply for the network permission. For details, see Preparations.

EntryAbility.ets

This file implements the app lifecycle. When starting an app, this file creates a connection between the RagSession and database. When closing an app, this file releases the connection between the RagSession and database.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/entryability/EntryAbility.ets
  2. import { AbilityConstant, UIAbility, Want, common } from '@kit.AbilityKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. import { window } from '@kit.ArkUI';
  5. import { BusinessError } from '@kit.BasicServicesKit';
  6. import SetUp from '../entryability/SetUp';
  7. import Config from '../entryability/Config';
  8. import { rag } from '@kit.DataAugmentationKit';
  9. const DOMAIN = 0x0000;
  10. export default class EntryAbility extends UIAbility {
  11. onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  12. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  13. }
  14. onDestroy(): void {
  15. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
  16. }
  17. onWindowStageCreate(windowStage: window.WindowStage): void {
  18. // Main window is created, set main page for this ability
  19. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
  20. windowStage.loadContent('pages/Index', (err) => {
  21. if (err.code) {
  22. hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
  23. return;
  24. }
  25. hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
  26. });
  27. AppStorage.setOrCreate<common.UIAbilityContext>('Context', this.context);
  28. let setUp: SetUp = new SetUp();
  29. setUp.initTable().then(() => {
  30. setUp.insertData();
  31. AppStorage.setOrCreate<SetUp>('SetUpObject', setUp);
  32. });
  33. let config: Config = new Config();
  34. rag.createRagSession(this.context, config.getRAGConfig()).then((data) => {
  35. AppStorage.setOrCreate<rag.RagSession>('RagSessionObject', data);
  36. }).catch((err: BusinessError) => {
  37. hilog.error(DOMAIN, 'testTag', `createRagSession failed, code is ${err.code},message is ${err.message}.`);
  38. });
  39. }
  40. onWindowStageDestroy(): void {
  41. // Main window is destroyed, release UI related resources
  42. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  43. const session = AppStorage.get<rag.RagSession>('RagSessionObject') as rag.RagSession;
  44. session?.close().catch(() => {
  45. hilog.error(DOMAIN, 'testTag', 'close rag session failed');
  46. });
  47. const setup = AppStorage.get<SetUp>('SetUpObject') as SetUp;
  48. setup?.closeStore();
  49. }
  50. onForeground(): void {
  51. // Ability has brought to foreground
  52. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
  53. }
  54. onBackground(): void {
  55. // Ability has back to background
  56. hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
  57. }
  58. }

SetUp.ets

The SetUp.ets file constructs data sources. Currently, this file reads data from a built-in JSON file and inserts the data into a database with knowledge processing enabled. After the data is updated, knowledge processing is automatically triggered to form a knowledge base.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/entryability/SetUp.ets
  2. import { UIAbility, common } from '@kit.AbilityKit';
  3. import { relationalStore } from '@kit.ArkData';
  4. import { buffer } from '@kit.ArkTS';
  5. import { hilog } from '@kit.PerformanceAnalysisKit';
  6. const TAG = 'SetUp';
  7. export default class SetUp extends UIAbility {
  8. storeName: string = 'testmail_store.db'; // Use the database name specified in the **knowledge_schema.json** file.
  9. storeConfig: relationalStore.StoreConfig = {
  10. name: this.storeName,
  11. securityLevel: relationalStore.SecurityLevel.S3,
  12. enableSemanticIndex: true, // For the source database, set this parameter to **true** to trigger knowledge processing.
  13. tokenizer: relationalStore.Tokenizer.CUSTOM_TOKENIZER
  14. };
  15. store?: relationalStore.RdbStore;
  16. async getStore() {
  17. try {
  18. if (!this.store) {
  19. let context = AppStorage.get<common.UIAbilityContext>('Context') as common.UIAbilityContext; // Obtain the global context.
  20. this.store = await relationalStore.getRdbStore(context, this.storeConfig);
  21. }
  22. } catch (err) {
  23. hilog.error(0, TAG, `Init DB failed, code is ${err.code},message is ${err.message}.`);
  24. }
  25. return this.store;
  26. }
  27. async initTable() {
  28. try {
  29. const tmpStore = await this.getStore();
  30. const createTableSql = 'CREATE TABLE IF NOT EXISTS email(id integer primary key, subject text, content text, ' +
  31. 'image_text text, attachment_names text, inline_files text, sender text, receivers text, received_date text);';
  32. await tmpStore?.execute(createTableSql, 0, undefined);
  33. hilog.info(0, TAG, 'InitTable success');
  34. } catch (err) {
  35. hilog.error(0, TAG, `Init DB failed, code is ${err.code},message is ${err.message}.`);
  36. }
  37. }
  38. async insertData() {
  39. try {
  40. let context = AppStorage.get<common.UIAbilityContext>('Context') as common.UIAbilityContext; // Obtain the global context.
  41. const fileList = context.resourceManager.getRawFileListSync('');
  42. let dataIndex = 0;
  43. for (let file of fileList) { // Parse data from the JSON file to the database.
  44. if (!file.startsWith('sourceData') || !file.endsWith('.json')) {
  45. hilog.info(0, TAG, `file ${file} skip`);
  46. continue;
  47. }
  48. hilog.info(0, TAG, `file ${file} start`);
  49. try {
  50. const rawFileData = await context.resourceManager.getRawFileContent(file);
  51. const fileData: string = buffer.from(rawFileData).toString();
  52. const resultObjArr = JSON.parse(fileData) as Array<object>;
  53. let jsonObj: object | undefined;
  54. for (let i = 0; i < resultObjArr.length; i++) {
  55. try {
  56. jsonObj = resultObjArr[i];
  57. let sender: string = jsonObj?.['sender_name'];
  58. if (!sender || sender.length == 0) {
  59. sender = 'undefined';
  60. }
  61. const receiverStr: string = JSON.stringify(jsonObj['to']);
  62. const formattedDateStr: string = jsonObj?.['received_time']?.replace(' ', 'T');
  63. let received_date = Date.parse(formattedDateStr);
  64. if (!received_date || Number.isNaN(received_date)) {
  65. received_date = 0;
  66. }
  67. let subject: string = jsonObj?.['subject']?.replace(/'/g, '');
  68. let doc: string = jsonObj?.['body']?.replace(/'/g, '');
  69. let sql = `insert or replace into email VALUES(${dataIndex}, '${subject}', '${doc}', '',` +
  70. ` '', '', '${sender}', '${receiverStr}', '${received_date}');`
  71. const tmpStore = await this.getStore();
  72. await tmpStore?.executeSql(sql);
  73. dataIndex++;
  74. } catch (e) {
  75. hilog.error(0, TAG, `Insert failed, code is ${e.code},message is ${e.message}, jsonObj: ${jsonObj}`);
  76. }
  77. }
  78. } catch (e) {
  79. hilog.error(0, TAG, `Load file failed, code is ${e.code},message is ${e.message}`);
  80. }
  81. hilog.info(0, TAG, `file ${file} end`);
  82. }
  83. hilog.info(0, TAG, 'insertData end');
  84. } catch (err) {
  85. hilog.error(0, TAG, `Init DB failed, code is ${err.code},message is ${err.message}.`);
  86. }
  87. }
  88. async closeStore() {
  89. try {
  90. await this.store?.close();
  91. } catch (e) {
  92. hilog.error(0, TAG, `Close store failed, code is ${e.code},message is ${e.message}.`);
  93. }
  94. }
  95. }

HttpUtils.ets

The HttpUtils.ets file contains an HTTP utility class for interacting with the LLM. It assembles packets to be sent to the LLM and registers the callback for receiving streaming HTTP messages. You can select a proper LLM as required. The sample code uses ModelArts. Replace "****replace your API key in here****" with the actual API key.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/entryability/HttpUtils.ets
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { http } from '@kit.NetworkKit';
  4. import { hilog } from '@kit.PerformanceAnalysisKit';
  5. const TAG = 'HttpUtils';
  6. class HttpUtils {
  7. httpRequest?: http.HttpRequest;
  8. url: string = 'https://api.modelarts-maas.com/v1/chat/completions'; // You need to change the URL and the model in the following content to those of the LLM you have chosen.
  9. isFinished: boolean = false;
  10. initOption(question: string) {
  11. let option: http.HttpRequestOptions = {
  12. // Request method
  13. method: http.RequestMethod.POST,
  14. //Request header
  15. header: {
  16. 'Content-Type': 'application/json',
  17. // API-KEY from Model
  18. 'Authorization': `Bearer ****replace your API key in here****`
  19. },
  20. //Request body
  21. extraData: {
  22. 'stream': true,
  23. 'temperature': 0.1,
  24. 'max_tokens': 1000,
  25. 'frequency_penalty': 1,
  26. 'model': 'qwen3-32b',
  27. 'top_p': 0.1,
  28. 'presence_penalty': -1,
  29. 'messages': JSON.parse(question),
  30. "chat_template_kwargs": {
  31. // Disable thinking.
  32. "enable_thinking": false
  33. }
  34. }
  35. };
  36. return option;
  37. }
  38. async requestInStream(question: string) { // Assemble the options of the streaming request and initiate the streaming request.
  39. if (!this.httpRequest) {
  40. this.httpRequest = http.createHttp();
  41. }
  42. this.httpRequest?.requestInStream(this.url, this.initOption(question)).catch((err: BusinessError) => {
  43. hilog.error(0, TAG, 'Failed to request. Cause: %{public}s', JSON.stringify(err));
  44. });
  45. this.isFinished = false;
  46. }
  47. on(callback: Callback<ArrayBuffer>) { // Register the data receiving and data ending listeners.
  48. if (!this.httpRequest) {
  49. this.httpRequest = http.createHttp();
  50. }
  51. this.httpRequest.on('dataReceive', callback);
  52. }
  53. end() { // Unregister the data receiving and data ending listeners and release the httpRequest.
  54. this.httpRequest?.off('dataReceive');
  55. this.httpRequest?.destroy();
  56. this.httpRequest = undefined;
  57. }
  58. cancel() {
  59. this.httpRequest?.off('dataReceive');
  60. this.httpRequest?.destroy();
  61. this.httpRequest = undefined;
  62. }
  63. }
  64. export default new HttpUtils;

MyChatLlm.ets

An app inherits from and implements the ChatLLM class, which is equivalent to the process of implementing an LLM client in the app. The LLM client will be passed as an input parameter to the RagSession during RagSession creation.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/entryability/MyChatLlm.ets
  2. import { rag } from '@kit.DataAugmentationKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. import { JSON, util } from '@kit.ArkTS';
  5. import HttpUtils from './HttpUtils';
  6. const TAG = "MyChatLLM";
  7. function parseLLMResponse(data: ArrayBuffer): rag.LLMStreamAnswer | undefined {
  8. try {
  9. let decoder = util.TextDecoder.create(`"utf-8"`);
  10. let str = decoder.decodeToString(new Uint8Array(data));
  11. hilog.info(0, TAG, str);
  12. let chunk = '';
  13. let isFinished: boolean = (str.length < 20);
  14. for (let resultStr of str.split('data:')) {
  15. if (resultStr.trim() == ('[DONE]')) {
  16. isFinished = true;
  17. break;
  18. }
  19. if (resultStr.trim().length == 0) {
  20. continue;
  21. }
  22. try {
  23. let obj = JSON.parse(resultStr.trim());
  24. if ((obj as object)?.['choices'].length === 0) {
  25. continue;
  26. }
  27. if ((obj as object)?.['choices'][0]?.['delta']?.['reasoning_content']) {
  28. chunk += (obj as object)?.['choices'][0]['delta']['reasoning_content'];
  29. } else if ((obj as object)?.['choices'][0]?.['delta']?.['content']) {
  30. chunk += (obj as object)?.['choices'][0]['delta']['content'];
  31. }
  32. } catch (err) {
  33. hilog.error(0, TAG, `Parse LLM response failed, resultStr: ${resultStr}`);
  34. }
  35. }
  36. let answer: rag.LLMStreamAnswer = {
  37. isFinished: isFinished,
  38. chunk: chunk
  39. };
  40. return answer;
  41. } catch (err) {
  42. hilog.error(0, TAG, `Parse LLM response failed, error code: ${err.code}, error message: ${err.message}`);
  43. }
  44. return undefined;
  45. }
  46. export default class MyChatLLM extends rag.ChatLLM {
  47. async streamChat(query: string, callback: Callback<rag.LLMStreamAnswer>): Promise<rag.LLMRequestInfo> {
  48. let ret: rag.LLMRequestStatus = rag.LLMRequestStatus.LLM_SUCCESS;
  49. try {
  50. let dataCallback = async (data: ArrayBuffer) => { // The callback function receives data, parses data, and assembles and returns **LLMStreamAnswer**.
  51. hilog.debug(0, TAG, 'on callback enter. data length: %{public}d', data.byteLength);
  52. // Parse the packet returned by the LLM. The logic varies depending on the selected model.
  53. const answer = parseLLMResponse(data);
  54. if (!answer) {
  55. return;
  56. }
  57. HttpUtils.isFinished = answer.isFinished;
  58. callback(answer);
  59. hilog.debug(0, 'MyChatLLM', 'Request LLM success. isFinished: %{public}s, data: %{public}s',
  60. Number(answer.isFinished).toString(), answer.chunk);
  61. };
  62. HttpUtils.on(dataCallback);
  63. HttpUtils.requestInStream(query);
  64. } catch (err) {
  65. hilog.error(0, TAG, `Request LLM failed, error code: ${err.code}, error message: ${err.message}`);
  66. ret = rag.LLMRequestStatus.LLM_REQUEST_ERROR; // This error code is for reference only. Your app can return other LLM error codes according to the service requirements.
  67. }
  68. return {
  69. chatId: 0,
  70. status: ret,
  71. };
  72. }
  73. cancel(chatId: number): void {
  74. hilog.info(0, TAG, `The request for the large model has been canceled. chatId: ${chatId}`);
  75. HttpUtils.cancel();
  76. }
  77. }

Config.ets

The Config.ets file assembles the input parameters used for creating a RagSession. For details about the configuration method and meaning, see Intelligent Data Retrieval.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/entryability/Config.ets
  2. import { common, UIAbility } from '@kit.AbilityKit';
  3. import { rag, retrieval } from '@kit.DataAugmentationKit';
  4. import { relationalStore } from '@kit.ArkData';
  5. import MyChatLlm from './MyChatLlm';
  6. export default class Config extends UIAbility {
  7. getRetrievalConfig() {
  8. let storeConfigVector: relationalStore.StoreConfig = {
  9. name: 'testmail_store_vector.db', // Name of the vector database file after knowledge processing, which is suffixed with **_vector** based on the original database name.
  10. securityLevel: relationalStore.SecurityLevel.S3,
  11. vector: true // Set this parameter to **true** for the vector database.
  12. };
  13. let storeConfigInvIdx: relationalStore.StoreConfig = {
  14. name: 'testmail_store.db', // Name of the inverted index database after knowledge processing. The inverted index database is the original database.
  15. securityLevel: relationalStore.SecurityLevel.S3,
  16. tokenizer: relationalStore.Tokenizer.CUSTOM_TOKENIZER
  17. };
  18. let context = AppStorage.get<common.UIAbilityContext>('Context') as common.UIAbilityContext;
  19. let channelConfigVector: retrieval.ChannelConfig = {
  20. channelType: retrieval.ChannelType.VECTOR_DATABASE,
  21. context: context,
  22. dbConfig: storeConfigVector
  23. };
  24. let channelConfigInvIdx: retrieval.ChannelConfig = {
  25. channelType: retrieval.ChannelType.INVERTED_INDEX_DATABASE,
  26. context: context,
  27. dbConfig: storeConfigInvIdx
  28. };
  29. let retrievalConfig: retrieval.RetrievalConfig = {
  30. channelConfigs: [channelConfigInvIdx, channelConfigVector]
  31. };
  32. return retrievalConfig;
  33. }
  34. getRetrivalCondition() {
  35. let recallConditionInvIdx: retrieval.InvertedIndexRecallCondition = {
  36. ftsTableName: 'email_inverted',
  37. fromClause: 'email_inverted',
  38. primaryKey: ['chunk_id'],
  39. responseColumns: ['reference_id', 'chunk_id', 'chunk_source', 'chunk_text'],
  40. deepSize: 500,
  41. recallName: 'invertedvectorRecall',
  42. };
  43. let floatArray = new Float32Array(128).fill(0.1);
  44. let vectorQuery: retrieval.VectorQuery = {
  45. column: 'repr',
  46. value: floatArray,
  47. similarityThreshold: 0.1
  48. };
  49. let recallConditionVector: retrieval.VectorRecallCondition = {
  50. vectorQuery: vectorQuery,
  51. fromClause: 'email_vector',
  52. primaryKey: ['id'],
  53. responseColumns: ['reference_id', 'chunk_id', 'chunk_source', 'repr'],
  54. recallName: 'vectorRecall',
  55. deepSize: 500
  56. };
  57. let rerankMethod: retrieval.RerankMethod = {
  58. rerankType: retrieval.RerankType.RRF,
  59. isSoftmaxNormalized: true,
  60. };
  61. let retrievalCondition: retrieval.RetrievalCondition = {
  62. rerankMethod: rerankMethod,
  63. recallConditions: [recallConditionInvIdx, recallConditionVector],
  64. resultCount: 5
  65. };
  66. return retrievalCondition;
  67. }
  68. getRAGConfig() {
  69. let retrievalConfig: retrieval.RetrievalConfig = this.getRetrievalConfig();
  70. let retrievalCondition: retrieval.RetrievalCondition = this.getRetrivalCondition();
  71. let config: rag.Config = {
  72. llm: new MyChatLlm(),
  73. retrievalConfig: retrievalConfig,
  74. retrievalCondition: retrievalCondition
  75. };
  76. return config;
  77. }
  78. }

Index.ets

The Index.ets file implements the app UI, including the question input text box, button for starting Q&A, and answer output text box.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/ets/pages/Index.ets
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { rag } from '@kit.DataAugmentationKit';
  4. import hilog from '@ohos.hilog';
  5. @Entry
  6. @Component
  7. struct Index {
  8. @State inputStr: string = 'Full sample code for the knowledge Q&A development guide';
  9. @State answerStr: string = '';
  10. @State thoughtStr: string = '';
  11. build() {
  12. Column() {
  13. Row({ space: 8 }) {
  14. TextArea({ text: this.inputStr, placeholder: 'Input question here!' })
  15. .margin({ top: 8 })
  16. .borderStyle(BorderStyle.Dotted)
  17. .onChange((newValue) => {
  18. this.inputStr = newValue;
  19. })
  20. .width('95%')
  21. .height('15%')
  22. .fontWeight(FontWeight.Bold)
  23. }
  24. Button('streamRun')
  25. .onClick(async () => {
  26. // Obtain the created RagSession.
  27. let session: rag.RagSession = AppStorage.get<rag.RagSession>('RagSessionObject') as rag.RagSession;
  28. let config: rag.RunConfig = {
  29. // Specify the streaming output type.
  30. answerTypes: [rag.StreamType.THOUGHT, rag.StreamType.ANSWER]
  31. };
  32. this.thoughtStr = '';
  33. this.answerStr = '';
  34. // Initiate a question.
  35. session.streamRun(this.inputStr, config, ((err: BusinessError, stream: rag.Stream) => {
  36. // Trigger the callback to receive the answer and process the answer information.
  37. if (err) {
  38. this.answerStr = `streamRun inner failed. code is ${err.code}, message is ${err.message}`;
  39. } else {
  40. // Select a processing method based on the data type.
  41. switch (stream.type) {
  42. case rag.StreamType.THOUGHT:
  43. this.thoughtStr += stream.answer.chunk;
  44. break;
  45. case rag.StreamType.ANSWER:
  46. this.answerStr += stream.answer.chunk;
  47. break;
  48. case rag.StreamType.REFERENCE:
  49. default:
  50. hilog.info(0, 'Index', `streamRun msg: ${JSON.stringify(stream)}`);
  51. }
  52. }
  53. })).catch((e: BusinessError) => {
  54. this.answerStr = `streamRun failed. code is ${e.code}, message is ${e.message}`;
  55. });
  56. })
  57. .width('30%')
  58. .height('5%')
  59. Column({ space: 2 }) {
  60. Text(this.thoughtStr)
  61. .fontSize(12)
  62. .fontColor(Color.Gray)
  63. .padding(8)
  64. .width('95%')
  65. .height('auto')
  66. Text(this.answerStr)
  67. .padding(8)
  68. .width('95%')
  69. .height('auto')
  70. }
  71. .backgroundColor(0xF5DEB3)
  72. .width('95%')
  73. .height('75%')
  74. }
  75. .height('100%')
  76. .width('100%')
  77. }
  78. }

knowledge_schema.json

This file is a schema file for knowledge processing, which is used to define the processing logic for the source database during knowledge processing.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/resources/rawfile/arkdata/knowledge/knowledge_schema.json ------ Delete this comment during app development.
  2. {
  3. "knowledgeSource": [{
  4. "version": 1,
  5. "dbName": "testmail_store.db",
  6. "tables": [{
  7. "tableName": "email",
  8. "referenceFields": ["id"],
  9. "knowledgeFields": [{
  10. "columnName": "subject",
  11. "type": ["Text"]
  12. },
  13. {
  14. "columnName": "content",
  15. "type": ["Text"]
  16. },
  17. {
  18. "columnName": "image_text",
  19. "type": ["Text"]
  20. },
  21. {
  22. "columnName": "attachment_names",
  23. "type": ["Text"]
  24. },
  25. {
  26. "columnName": "inline_files",
  27. "type": ["Json"],
  28. "parser": [
  29. {
  30. "type": "File",
  31. "path": "$[*].uri"
  32. }
  33. ]
  34. },
  35. {
  36. "columnName": "sender",
  37. "type": ["Scalar"],
  38. "description": "sender"
  39. },
  40. {
  41. "columnName": "receivers",
  42. "type": ["Scalar"],
  43. "description": "receivers"
  44. },
  45. {
  46. "columnName": "received_date",
  47. "type": ["Scalar"],
  48. "description": "received_date"
  49. }]
  50. }]
  51. }]
  52. }

sourceData.json

The content in the sourceData.json file is the simulated source data for testing, which is inserted into the app database table. In actual scenarios, app data is specified on the UI or obtained from the server.

Collapse
Word wrap
Dark theme
Copy code
  1. // src/main/resources/rawfile/sourceData.json ------ This file is used only for inserting test data. Preconfigure database data as required.
  2. [{
  3. "subject": "Mobile Phone Discount Policy",
  4. "sender_name": "test1",
  5. "sender_email": "test1@example.com",
  6. "received_time": "2025-05-15 15:49:04.135",
  7. "recipients": [
  8. {
  9. "Address": "test2@example.com",
  10. "name": "test2",
  11. "Type": 1
  12. },
  13. {
  14. "Address": "test3@example.com",
  15. "name": "test3",
  16. "Type": 2
  17. },
  18. {
  19. "Address": "test4@example.com",
  20. "name": "test4",
  21. "Type": 3
  22. }
  23. ],
  24. "to": [
  25. "lisi"
  26. ],
  27. "cc": [
  28. "wangwu"
  29. ],
  30. "bcc": [
  31. "zhaoliu"
  32. ],
  33. "attachment": [],
  34. "body": "Preference policy: \r\n10% discount for flagship series; 20% discount for non-flagship series.",
  35. "unread": false
  36. }]
Search in Guides
Enter a keyword.