Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

document chat #20

Merged
merged 3 commits into from Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view

Large diffs are not rendered by default.

Expand Up @@ -28,6 +28,7 @@ import androidx.lifecycle.viewModelScope
import com.example.compose.jetchat.components.Channel
import com.example.compose.jetchat.conversation.ConversationUiState
import com.example.compose.jetchat.conversation.Message
import com.example.compose.jetchat.data.initialDocumentMessages
import com.example.compose.jetchat.data.initialDroidconMessages
import com.example.compose.jetchat.data.initialOpenAiMessages
import com.example.compose.jetchat.data.initialPalmMessages
Expand Down Expand Up @@ -71,6 +72,7 @@ class MainViewModel : ViewModel() {
context = ctx
droidconWrapper = DroidconEmbeddingsWrapper(context)
openAIWrapper = OpenAIWrapper(context)
documentWrapper = DocumentChatWrapper(context)
}

fun openDrawer() {
Expand Down Expand Up @@ -109,6 +111,15 @@ class MainViewModel : ViewModel() {
)
)

private val documentUiState by mutableStateOf(
ConversationUiState(
initialMessages = initialDocumentMessages,
channelName = Channel.DOCUMENT.label,
channelMembers = 2,
channelBotProfile = openAiProfile
)
)

var currentChannel by mutableStateOf(Channel.OPENAI)

val uiState: ConversationUiState
Expand All @@ -117,12 +128,14 @@ class MainViewModel : ViewModel() {
Channel.PALM -> palmUiState
Channel.OPENAI -> openAiUiState
Channel.DROIDCON -> droidconUiState
Channel.DOCUMENT -> documentUiState
}
}
private var openAIWrapper = OpenAIWrapper(null)
private var palmWrapper = PalmWrapper()
/** Requires a `context` for database operations. Set post-init with `setContext` function call */
private var droidconWrapper = DroidconEmbeddingsWrapper(null) // no context available for database functions in DroidconEmbeddingsWrapper, added later by `setContext` function
private var documentWrapper = DocumentChatWrapper(null)

var botIsTyping by mutableStateOf(false)
private set
Expand Down Expand Up @@ -156,6 +169,7 @@ class MainViewModel : ViewModel() {
when (currentChannel) {
Channel.PALM -> palmWrapper.chat(content)
Channel.DROIDCON -> droidconWrapper.chat(content)
Channel.DOCUMENT -> documentWrapper.chat(content)
else -> openAIWrapper.chat(content)
}
} catch (e: Exception) {
Expand Down
Expand Up @@ -60,6 +60,7 @@ enum class Channel(val label: String) {
OPENAI("#jetchat-ai"),
PALM("#jetchat-palm"),
DROIDCON("#droidcon-chat"),
DOCUMENT("#document-chat")
}

@Composable
Expand Down
@@ -0,0 +1,87 @@
package com.example.compose.jetchat.data

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import android.util.Log

/* Following this guide
* https://developer.android.com/training/data-storage/sqlite
*/

/** Database schema for documents */
object DocumentContract {
// Table contents are grouped together in an anonymous object.

object EmbeddingEntry : BaseColumns {
const val TABLE_NAME = "embedding"
const val COLUMN_NAME_CHUNKID = "chunk_id"
const val COLUMN_NAME_CONTENT = "content"
const val COLUMN_NAME_VECTOR = "vector"
}
}

//-- Embeddings
private const val SQL_CREATE_EMBEDDING_ENTRIES =
"CREATE TABLE ${DocumentContract.EmbeddingEntry.TABLE_NAME} (" +
"${DocumentContract.EmbeddingEntry.COLUMN_NAME_CHUNKID} TEXT PRIMARY KEY," +
"${DocumentContract.EmbeddingEntry.COLUMN_NAME_CONTENT} TEXT," +
"${DocumentContract.EmbeddingEntry.COLUMN_NAME_VECTOR} TEXT)"

private const val SQL_DELETE_EMBEDDING_ENTRIES = "DROP TABLE IF EXISTS ${DroidconContract.EmbeddingEntry.TABLE_NAME}"



class DocumentDbHelper(var context: Context?) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

override fun onCreate(db: SQLiteDatabase) {
db.execSQL(SQL_CREATE_EMBEDDING_ENTRIES)
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
//db.execSQL(SQL_DELETE_SESSION_ENTRIES)
db.execSQL(SQL_DELETE_EMBEDDING_ENTRIES)
onCreate(db)
}

override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, oldVersion, newVersion)
}

companion object {
// If you change the database schema, you must increment the database version.
const val DATABASE_VERSION = 2
const val DATABASE_NAME = "DocumentChat.db"
}

/** Generates the database schema for the `AskDatabaseFunction` */
fun generateSimpleSchema(): String {
val db = readableDatabase
var out = ""
// get_table_names
val tableCursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null)
with(tableCursor) {
while (moveToNext()) {
val tableName = getString(0)
out += "Table: $tableName\nColumns: "
// get_column_names
val columnCursor = db.rawQuery("PRAGMA table_info('$tableName');", null)
var needComma = false
with(columnCursor) {
while (moveToNext()) {
val columnName = getString(1)
if (needComma) out += ", " else needComma = true
out += "$columnName"
}
}
columnCursor.close()
out += "\n\n"
}
}
tableCursor.close()
return out
}
}
Expand Up @@ -103,6 +103,16 @@ val initialDroidconMessages =
)
)

val initialDocumentMessages =
listOf(
Message(
openAiProfile.displayName,
"Welcome to #document-chat! Ask questions about Contoso employee policy documents...",
"9:00 am",
authorImage = openAiProfile.photo
)
)

private val initialMessages = listOf(
Message(
"me",
Expand Down
Expand Up @@ -118,6 +118,8 @@ class AskDatabaseFunction {
}
out += ")\n"
rowCount++

if (rowCount > 8) break // HACK: to control size
}
out += "]"
Log.i("LLM", "askDatabase rowCount: $rowCount")
Expand Down
@@ -0,0 +1,136 @@
package com.example.compose.jetchat.functions

import android.database.Cursor.FIELD_TYPE_INTEGER
import android.util.Log
import com.aallam.openai.api.chat.Parameters
import com.example.compose.jetchat.data.DocumentDbHelper
import kotlinx.serialization.json.add
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject

/* Inspired by OpenAI example
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb
*
* Uses
* https://developer.android.com/training/data-storage/sqlite
* */
/** Function that exposes documents info database schema and lets the model query it
* directly with SQL and then parse the results to answer questions */
class AskDocumentFunction {
companion object {
fun name(): String {
return "askDatabase"
}

fun description(): String {
return "Answer user questions about document contents by searching for a keyword in the document database. Output should be a fully formed SQL query."
}

fun params(db: DocumentDbHelper): Parameters {
val schema = db.generateSimpleSchema()
//Log.v("LLM", "params db schema:\n$schema")
val params = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("query") {
put("type", "string")
put(
"description", """
SQL query extracting info to answer the user's question.
SQL should be written using this database schema:

$schema

The query should be valid SQL returned in plain text, not in JSON.
""".trimIndent()
)
}
}
putJsonArray("required") {
add("query")
}
}
return params
}
/** hardcoded demo schema to test model's comprehension */
@Deprecated("Method used for prototyping and observing the model's responses to schema changes")
fun paramsTest(): Parameters {
val params = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("query") {
put("type", "string")
put(
"description", """
SQL query extracting info to answer the user's question.
SQL should be written using this database schema:

Table: sessions
Columns: session_id, speaker, role, location_id, date, time, subject, description

Table: favorites
Columns: session_id, is_favorite

Table: locations
Columns: location_id, directions

The query should be returned in plain text, not in JSON.
""".trimIndent()
)
}
}
putJsonArray("required") {
add("query")
}
}
return params
}

/**
* Execute arbitrary queries against a local database
*/
fun function(dbHelper: DocumentDbHelper, query: String): String {
Log.i("LLM", "askDatabase ($query)")

val db = dbHelper.readableDatabase
val cursor = db.rawQuery(query,null)
var rowCount = 0
var out = ""
var needOuterComma = false
with(cursor) {
// mimicking the output format from the OpenAI Cookbook
// eg. [('abc', 1),('def', 2)]
out += "["
var needComma = false
while (moveToNext()) {
if (needOuterComma) out += "," else needOuterComma = true
out += "("
for (i in 0 until cursor.columnCount) {
if (needComma) out += "," else needComma = true
out += when (getType(i)) {
FIELD_TYPE_INTEGER ->
getString(i)
else -> {
"'${getString(i)}'"
}
}
}
out += ")\n"
rowCount++
}
out += "]"
Log.i("LLM", "askDatabase rowCount: $rowCount")
Log.v("LLM", " $out")
}
cursor.close()

return if (out == "") {
Log.i("LLM", "askDatabase rowCount: 0")
"0 rows affected"
} else {
out
}
}
}
}
23 changes: 23 additions & 0 deletions Jetchat/app/src/main/res/raw/benefit_options.txt
@@ -0,0 +1,23 @@
Contoso Electronics Plan and Benefit Packages

This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document.
All rights reserved to Microsoft

Welcome to Contoso Electronics! We are excited to offer our employees two comprehensive health insurance plans through Northwind Health.
Northwind Health Plus
Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan also offers coverage for emergency services, both in-network and out-of-network.
Northwind Standard
Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services.
Comparison of Plans
Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings.
Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services.
Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand- name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs.
Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses.
Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests.
Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also

offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services.
Cost Comparison
Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics:
Next Steps
We hope that this information has been helpful in understanding the differences between Northwind Health Plus and Northwind Standard. We are confident that you will find the right plan for you and your family. Thank you for choosing Contoso Electronics!