Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Changelog

## [2.0.3]

### Added
- **Chat conversations can be deleted one at a time, in a selected group, or all at once.** Each conversation in the chat sidebar has a delete button, *Select* lets several conversations be picked (or all shown ones at once) and deleted together, and a new *Clear all* action removes the whole history. The only option before, *Clear older*, always kept the ten most recent conversations and appeared only once there were more than ten.

### Fixed
- **Gemini models no longer run tools on their own.** The Gemini SDK's automatic function calling was left on, so the SDK could execute tool calls itself instead of returning them to the agent. It is now disabled, so planning, tracing and step order apply to every tool call.
- **Agentic mode is used only with models that actually call tools.** The capability check counted any accepted request as support, so an OpenAI-compatible server that ignores tools ran in Agentic mode and then failed to plan. The check now requires the model to call the tool, asking a second time with the call required, and such models use Classic mode.
- **Contextual search returns each passage once, with its correct position.** When two matched chunks were close together in a document, the passages around them were returned under both matches with wrong, inconsistent positions (a match could appear several places away from itself), so the answer saw duplicated context. Each chunk is now listed once, under the nearest match, with its exact offset. Existing graphs pick this up by running Repair in the Migration Assistant.
- **Knowledge-graph rebuild works when signed in with a token.** A rebuild started from a token sign-in could fail at once, reported as the graph not existing or not being accessible. It now runs normally, and an invalid token is rejected as an authentication failure rather than a server error.
- **Contextual document search honours the search options it was given.** The option to use a hypothetical-answer embedding and the option to expand the question were swapped before reaching contextual (sibling-chunk) search, so asking for one applied the other — and each selects a different search strategy, not a variation of one. Requests that set neither option, including all chat traffic, were unaffected.
- **Document search now requires valid credentials.** The document-retrieval endpoint returned matching content without checking who was asking, and searched the configured default graph rather than the one named in the request. It now rejects callers who cannot access the requested graph, and searches that graph.
- **Tables in Word documents are no longer dropped on upload.** Only the paragraphs of a `.docx` were read, so every table vanished while the text around it was ingested normally — a document could state that limits are defined below and contain none of them. Tables are now extracted in place, so the figures they hold can be found and cited.
- **A document that fails to extract is no longer ingested as its own error message.** When a PDF or image could not be read, the extractor produced a document whose text was the failure notice — which was then chunked, embedded and returned by search like ordinary content. Nothing is ingested for such a file now, so the knowledge graph contains no placeholder text and answers can't be drawn from one.
- **Files that fail to extract are no longer counted as ingested.** An unreadable PDF or image was previously counted as a successfully ingested document, so the upload looked clean. It is now left out, the rest of the upload still processes, and the file and its reason are returned with the upload result. When none of the files can be read, the upload stops and gives the reasons.
- **Legacy `.doc` files are no longer accepted as if they could be read.** The format was listed as supported but could never be read, so such a file was ingested as a one-line note saying it was unsupported. The file picker now marks `.doc` as unsupported, and one sent through the API is reported as unreadable, with a pointer to `.docx`.
- **An empty spreadsheet is reported rather than ingested as a note about itself.**
- **Conversation context on the query-with-history endpoint no longer crosses callers.** `/{graphname}/query_with_history` kept a single conversation history for the whole service, so concurrent callers — on the same graph or different ones — had each other's previous questions and answers fed into their prompt. Context is now taken from the request instead of being retained on the server.
- **Deleting an MCP server takes effect, after a confirmation.** The delete button removed a server only from the page, without asking, and there was no way to save the change, so the server reappeared on reload. Deleting now asks first and is saved immediately.
- **Cancelling an MCP server edit no longer deletes the server.** Cancel removed an existing server from the list, so the next save of any server deleted it. Cancel now restores the saved settings.
- **Agentic answers no longer wait on a rejected planning request.** With an OpenAI chat model, each plan was first requested in a form OpenAI rejects and then retried, adding latency and token cost to every agentic answer. Plans are now requested in a form it accepts.
- **Answers no longer fail when retrieved context exceeds the model's input limit.** With no `token_limit` configured — the default in the sample configs — retrieved context was sent untrimmed, so a large retrieval could be rejected by the model and the user got a generic failure reply. When `token_limit` is unset, the chat model's own input limit now applies. Trimming also reserves room for the full prompt and counts tokens rather than characters, so Japanese and other CJK context is trimmed correctly.
- **Agentic answers no longer pay for the same passage twice.** When several search steps of a plan returned the same document passage, the answer model received every copy. Each passage is now included once, in the step that found it first.
- **Search steps in the trace report what they retrieved.** An agentic search step's summary showed a fixed "2 item(s)" regardless of results; it now gives the number and total size of the chunks retrieved (and of any other entries), and a search that finds nothing is reported as empty. Each search also logs the same line. A step output too large to keep in full in the trace is now shown as the tool's own data, shortened, with a note giving its full and shown size and each shortened text ending in how much of it was left out — previously it was replaced by a preview marker that read as if the tool's output itself had been cut.

### Changed
- **Requires pyTigerGraph 2.0.5 and tigergraph-mcp 1.0.4.** pyTigerGraph 2.0.5 reuses async connections safely across event loops and submits query installation asynchronously; tigergraph-mcp 1.0.4 is the agent's in-process TigerGraph tool server.
- **Agentic answers handle multi-part and follow-up questions better.** Each part of a multi-part question is searched on its own and answered from its matching source, references to earlier turns are resolved from the conversation, and when a reference is ambiguous the assistant asks one short clarifying question instead of guessing.
- **The Migration Assistant repairs only graphs it can repair in place.** Its check now verifies that the graph has the schema GraphRAG's queries are written against, which every graph created with GraphRAG 1.4 or later has. A graph created with an earlier version is reported as needing to be recreated and its documents ingested again, and Repair is not offered — previously Repair would replace its working queries with ones its schema cannot run. If the schema cannot be read, Repair is withheld until the check succeeds.
- **`token_limit: 0` no longer means unlimited.** A `token_limit` of `0` or left unset now uses the chat model's own input limit (less room for the answer), so retrieved context is trimmed to what the model accepts. Models whose limit is not known keep the previous untrimmed behavior. Set a positive `token_limit` to choose a lower cap.
- **Callers of `/{graphname}/query_with_history` supply their own conversation context.** The request body accepts an optional `history` list of `{"query", "response"}` turns, oldest first, and the most recent turns are used. A request that omits it is answered without prior context; the endpoint no longer remembers anything between calls. Callers that relied on the server accumulating history must now send the turns they want considered.

## [2.0.2]

### Added
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [Use TigerGraph GraphRAG](#use-tigergraph-graphrag)
- [Run Demo with Preloaded GraphRAG](#run-demo-with-preloaded-graphrag)
- [Manually Build GraphRAG From Scratch](#manually-build-graphrag-from-scratch)
- [Upgrade an Existing Graph](#upgrade-an-existing-graph)
- [Chat Engines and Agents](#chat-engines-and-agents)
- [Agentic](#agentic)
- [Classic](#classic)
Expand Down Expand Up @@ -67,6 +68,7 @@
---

## Releases
* **10/7/2026**: GraphRAG v2.0.3 released. Improved agentic answers to multi-part and follow-up questions, kept answers within the chat model's input limit, added deleting chat conversations individually, in groups, or all at once, made the Migration Assistant check a graph's schema before repairing it, and fixed document retrieval, ingestion, and contextual search issues. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v2.0.3) for details.
* **8/28/2026**: GraphRAG v2.0.2 released. Added Migration Assistant data-integrity checks with targeted re-embedding of missing embeddings and re-summarization of incomplete community summaries, made agentic chat work with current and future tool-calling models automatically, and fixed rebuild, PDF extraction, and chat issues. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v2.0.2) for details.
* **7/22/2026**: GraphRAG v2.0.1 released. Fixed a chat error with models that return structured content (such as Gemini 3), and hardened knowledge-graph rebuild so it no longer hangs when the language model is unreachable. Relicensed from Apache 2.0 to AGPL-3.0. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v2.0.1) for details.
* **7/1/2026**: GraphRAG v2.0.0 released. Added an agentic chat engine that plans and runs its own retrieval (Planner and Reactive styles), external MCP tools, and structure-aware document chunking, along with additive prompt customization and many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v2.0.0) for details.
Expand Down Expand Up @@ -364,6 +366,19 @@ The script will:
1. Init the GraphRAG based on the graph and install required queries
1. Ask a question via Python to get answer from GraphRAG

### Upgrade an Existing Graph

Upgrading GraphRAG does not change graphs that already exist: their installed queries stay as they were until they are repaired. After upgrading, open **Knowledge Graph Admin → Migration Assistant** and check each graph. The check reports:

- **Schema compatibility** — whether the graph's schema supports the current release. Graphs created with GraphRAG 1.4 or later are compatible.
- **Queries** — installed queries that are outdated or missing. **Repair** reinstalls them in place, without rebuilding the knowledge graph.
- **Prompt overrides** — saved custom prompts in a format the current release no longer uses.
- **Data health** — vertices missing embeddings and communities missing summaries, each with its own regenerate action.

A graph created with a version earlier than 1.4 can't be repaired in place; the Migration Assistant reports this and does not offer **Repair**. Create a new graph and ingest its documents again.

Repair can't run while the graph is being rebuilt or ingested.

[Go back to top](#top)

---
Expand Down Expand Up @@ -666,7 +681,7 @@ In the `llm_config` section of `configs/server_config.json` file, copy JSON conf
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `authentication_configuration` | object | — | Shared authentication credentials for all services. Service-level values take precedence. |
| `token_limit` | int | — | Hard cap on token count for retrieved context sent to the LLM. Context exceeding this limit is truncated. Inherited by all services if not set at service level. `0` or omitted means unlimited. |
| `token_limit` | int | — | Hard cap on token count for a prompt carrying retrieved context, prompt included. Context beyond it is truncated. Inherited by all services if not set at service level. `0` or omitted uses the chat model's own input limit when known, otherwise no cap. |

**`completion_service` parameters:**

Expand All @@ -678,7 +693,7 @@ In the `llm_config` section of `configs/server_config.json` file, copy JSON conf
| `model_kwargs` | object | No | `{}` | Additional model parameters (e.g., `{"temperature": 0}`). |
| `prompt_path` | string | No | `"./common/prompts/openai_gpt4/"` | Path to prompt template files. |
| `base_url` | string | No | — | Custom API endpoint URL. |
| `token_limit` | int | No | inherited from top-level | Hard cap on token count for retrieved context sent to the LLM. Context exceeding this limit is truncated. `0` or omitted means unlimited. |
| `token_limit` | int | No | inherited from top-level | Hard cap on token count for a prompt carrying retrieved context, prompt included. Context beyond it is truncated. `0` or omitted uses the model's own input limit when known, otherwise no cap. |

**`embedding_service` parameters:**

Expand All @@ -701,7 +716,7 @@ Chatbot LLM override. If not configured, inherits from `completion_service`. Con
| `model_kwargs` | object | No | inherited from completion | Additional model parameters (e.g., `{"temperature": 0}`). |
| `prompt_path` | string | No | inherited from completion | Path to prompt template files. |
| `base_url` | string | No | inherited from completion | Custom API endpoint URL. |
| `token_limit` | int | No | inherited from completion | Hard cap on token count for retrieved context sent to the chatbot LLM. Context exceeding this limit is truncated. `0` or omitted means unlimited. |
| `token_limit` | int | No | inherited from completion | Hard cap on token count for a prompt carrying retrieved context to the chatbot LLM, prompt included. Context beyond it is truncated. `0` or omitted uses the model's own input limit when known, otherwise no cap. |

**`multimodal_service` parameters (optional):**

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.2
2.0.3
27 changes: 27 additions & 0 deletions chat-history/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ func UpdateConversationById(message structs.Message) (*structs.Conversation, err
return &convo, nil
}

// DeleteUserConversation removes one conversation and its messages when it
// belongs to userId. Missing and other users' conversations are not deleted.
func DeleteUserConversation(userId, conversationId string) (bool, error) {
mu.Lock()
defer mu.Unlock()

var convo structs.Conversation
tx := db.Where("user_id = ? AND conversation_id = ?", userId, conversationId).First(&convo)
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
return false, nil
}
if tx.Error != nil {
return false, tx.Error
}

err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("conversation_id = ?", conversationId).Delete(&structs.Message{}).Error; err != nil {
return err
}
return tx.Delete(&convo).Error
})
if err != nil {
return false, err
}
return true, nil
}

// GetAllMessages retrieves all messages from the database
func GetAllMessages() ([]structs.Message, error) {
var messages []structs.Message
Expand Down
1 change: 1 addition & 0 deletions chat-history/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func main() {

router.HandleFunc("GET /user/{userId}", routes.GetUserConversations)
router.HandleFunc("GET /conversation/{conversationId}", routes.GetConversation)
router.HandleFunc("DELETE /conversation/{conversationId}", routes.DeleteConversation)
router.HandleFunc("POST /conversation", routes.UpdateConversation)
router.HandleFunc("GET /get_feedback", routes.GetFeedback(cfg.TgDbConfig.Hostname, cfg.TgDbConfig.GsPort, cfg.ChatDbConfig.ConversationAccessRoles))

Expand Down
26 changes: 26 additions & 0 deletions chat-history/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ func UpdateConversation(w http.ResponseWriter, r *http.Request) {
}
}

// Delete a conversation and its messages.
// "DELETE /conversation/{conversationId}"
func DeleteConversation(w http.ResponseWriter, r *http.Request) {
conversationId := r.PathValue("conversationId")
userId, code, reason, ok := auth("", r)
if !ok {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(reason)
return
}

deleted, err := db.DeleteUserConversation(userId, conversationId)
if err != nil {
panic(err)
}
w.Header().Add("Content-Type", "application/json")
if !deleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"reason":"conversation not found"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"Conversation deleted successfully"}`))
}

// Basic auth helper
func auth(userId string, r *http.Request) (string, int, []byte, bool) {
usr, _, ok := r.BasicAuth()
Expand Down
75 changes: 75 additions & 0 deletions chat-history/routes/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,81 @@ func TestGetConversation(t *testing.T) {
}
}

func TestDeleteConversation(t *testing.T) {
setupDB(t, true)
mux := http.NewServeMux()
mux.HandleFunc("DELETE /conversation/{conversationId}", DeleteConversation)
mux.HandleFunc("GET /conversation/{conversationId}", GetConversation)
mux.HandleFunc("GET /user/{userId}", GetUserConversations)

req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/conversation/%s", CONVO_ID), nil)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthSetup(USER, PASS)))
resp := httptest.NewRecorder()
mux.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("Response code should be 200. It is: %v %s", resp.Code, body)
}

req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/conversation/%s", CONVO_ID), nil)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthSetup(USER, PASS)))
resp = httptest.NewRecorder()
mux.ServeHTTP(resp, req)
var messages []structs.Message
if err := json.Unmarshal(resp.Body.Bytes(), &messages); err != nil {
t.Fatal(err)
}
if len(messages) != 0 {
t.Fatalf("deleted conversation still has %d messages", len(messages))
}

req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/user/%s", USER), nil)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthSetup(USER, PASS)))
resp = httptest.NewRecorder()
mux.ServeHTTP(resp, req)
var convos []structs.Conversation
if err := json.Unmarshal(resp.Body.Bytes(), &convos); err != nil {
t.Fatal(err)
}
for _, c := range convos {
if c.ConversationId.String() == CONVO_ID {
t.Fatal("deleted conversation is still in the user list")
}
}

// Another user's conversation is left alone.
other := "601529eb-4927-4e24-b285-bd6b9519a952"
req = httptest.NewRequest(http.MethodDelete, "/conversation/"+other, nil)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthSetup(USER, PASS)))
resp = httptest.NewRecorder()
mux.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Fatalf("deleting another user's conversation should be 404. It is: %v", resp.Code)
}
req = httptest.NewRequest(http.MethodGet, "/conversation/"+other, nil)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuthSetup("Miss_Take", PASS)))
resp = httptest.NewRecorder()
mux.ServeHTTP(resp, req)
if err := json.Unmarshal(resp.Body.Bytes(), &messages); err != nil {
t.Fatal(err)
}
if len(messages) == 0 {
t.Fatal("another user's conversation was deleted")
}
}

func TestDeleteConversation_401(t *testing.T) {
setupDB(t, true)
mux := http.NewServeMux()
mux.HandleFunc("DELETE /conversation/{conversationId}", DeleteConversation)
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/conversation/%s", CONVO_ID), nil)
resp := httptest.NewRecorder()
mux.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Fatalf("Response code should be 401. It is: %v", resp.Code)
}
}

func TestGetConversation_401(t *testing.T) {
// setup
setupDB(t, false)
Expand Down
Loading
Loading