Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds project management tools to the GeoAgent QGIS plugin, enabling users to create, save, and load QGIS projects through the AI agent interface.
- Introduces three new project management functions:
create_new_qgis_project,save_qgis_project, andload_qgis_project - Implements thread-safe project loading with a new
ProjectLoadDispatcherthat handles project operations on the main Qt thread - Modifies the agent graph to change tool result handling from routing back to the LLM to ending directly after tool execution
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/project_loader.py | New module providing thread-safe QGIS project loading with ProjectLoadDispatcher and callback infrastructure |
| utils/canvas_refresh.py | Minor grammar fix in comment ("it will" → "will") |
| tools/io.py | Renames new_qgis_project to create_new_qgis_project and adds save_qgis_project and load_qgis_project functions with validation |
| tools/init.py | Updates exports to include renamed and new project management functions |
| llm/worker.py | Changes result signal type from AIMessage to generic object to support both AIMessage and ToolMessage |
| geo_agent.py | Adds ProjectLoadDispatcher initialization and _project_load_callback implementation with event loop synchronization |
| agents/graph.py | Enhances tool node error handling and changes graph edge from "tools" → "llm" to "tools" → END |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self._project_load_dispatcher.result_ready.connect(on_result_ready) | ||
|
|
||
| # queue the load operation on the main thread | ||
| QMetaObject.invokeMethod( | ||
| self._project_load_dispatcher, | ||
| "doLoadProject", | ||
| Qt.QueuedConnection, | ||
| Q_ARG(str, path), | ||
| ) | ||
|
|
||
| # wait for the dispatcher to signal completion | ||
| loop = QEventLoop() | ||
|
|
||
| # max wait 30 seconds timeout | ||
| QTimer.singleShot(30000, loop.quit) | ||
| loop.exec_() | ||
|
|
||
| # disconnect signal to avoid memory leaks | ||
| self._project_load_dispatcher.result_ready.disconnect(on_result_ready) | ||
|
|
||
| # return the result that was set by the dispatcher | ||
| return self._project_load_dispatcher.result |
There was a problem hiding this comment.
There's a potential race condition: if an exception occurs during QMetaObject.invokeMethod or the timeout, the signal will remain connected, causing a memory leak. The disconnect should be in a finally block to ensure it always executes, or use a try-finally pattern around the entire signal handling logic.
| - save_qgis_project('/geoagent/my_project.qgs') | ||
| - save_qgis_project('D:/geoagent/project_backup.qgz') |
There was a problem hiding this comment.
There are two trailing spaces at the end of these comment lines, which is inconsistent with the rest of the codebase and can cause issues with some linters and version control systems.
| - save_qgis_project('/geoagent/my_project.qgs') | |
| - save_qgis_project('D:/geoagent/project_backup.qgz') | |
| - save_qgis_project('/geoagent/my_project.qgs') | |
| - save_qgis_project('D:/geoagent/project_backup.qgz') |
| ) | ||
|
|
||
| # wait for the dispatcher to signal completion | ||
| loop = QEventLoop() |
There was a problem hiding this comment.
The QEventLoop is created after it's used in the on_result_ready function closure. This will cause a NameError when on_result_ready is called because 'loop' is not yet defined at the time the function is created. The loop variable should be created before defining the on_result_ready function.
| except: | ||
| pass |
There was a problem hiding this comment.
Bare except clause silently catches all exceptions including system-exiting exceptions like KeyboardInterrupt and SystemExit. This can mask serious errors and make debugging difficult. Consider catching specific exception types or at least logging the exception before suppressing it.
| except: | ||
| pass |
There was a problem hiding this comment.
Bare except clause silently catches all exceptions including system-exiting exceptions like KeyboardInterrupt and SystemExit. This can mask serious errors and make debugging difficult. Consider catching specific exception types or at least logging the exception before suppressing it.
| loop = QEventLoop() | ||
|
|
||
| # max wait 30 seconds timeout | ||
| QTimer.singleShot(30000, loop.quit) |
There was a problem hiding this comment.
The magic number 30000 (30 seconds) is used directly without explanation. Consider defining it as a named constant like PROJECT_LOAD_TIMEOUT_MS = 30000 to improve code readability and maintainability.
| graph.set_entry_point("llm") | ||
| graph.add_conditional_edges("llm", should_use_tools, {"tools": "tools", END: END}) | ||
| graph.add_edge("tools", "llm") | ||
| graph.add_edge("tools", END) |
There was a problem hiding this comment.
Changing the graph edge from "tools" → "llm" to "tools" → END fundamentally alters the agent's behavior. Previously, tool results would be sent back to the LLM for interpretation and natural language response generation. Now, tool execution results go directly to END, meaning the last message will be a ToolMessage with raw tool output instead of an AIMessage with a natural language response. This breaks the conversational flow as users will see raw tool outputs like "Success: Layer added..." instead of the AI interpreting and presenting results naturally.
| graph.add_edge("tools", END) | |
| graph.add_edge("tools", "llm") |
| # max wait 30 seconds timeout | ||
| QTimer.singleShot(30000, loop.quit) | ||
| loop.exec_() |
There was a problem hiding this comment.
The timeout timer is started but there's no mechanism to stop it if the result_ready signal fires first. This means the timer will continue to run and could potentially call loop.quit() even after the loop has already quit from the signal, which could cause issues. Consider storing the timer and calling stop() on it after the loop exits.
| Graph builder for GeoAgent: routes between general conversational mode and geoprocessing workflows. | ||
| """ | ||
| from typing import List, Any | ||
| from langchain_core.messages import SystemMessage, HumanMessage |
There was a problem hiding this comment.
Import of 'SystemMessage' is not used.
Import of 'HumanMessage' is not used.
| from langchain_core.messages import SystemMessage, HumanMessage |
|
@rabenojha can you work on Copilot suggestions? Let's try to implement the good tips and ignore unnecessary suggestions. |
Logger and structured output
gemini default model switched from pro to flash
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 26 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _logger.info(f"Successfully loaded project from '{path}'") | ||
| return f"Success: Project loaded from '{path}'" | ||
| else: | ||
| _logger.error(f"Unknown error loading project from '{path}'") |
There was a problem hiding this comment.
The function returns None at the end when it should return an error message. After the except block catches an exception and logs it at line 527, control falls through without a return statement. This means the function will return None instead of an error message string, which is inconsistent with the function's return type annotation and other error paths.
| _logger.error(f"Unknown error loading project from '{path}'") | |
| _logger.error(f"Unknown error loading project from '{path}'") | |
| return f"Error: Unknown error loading project from '{path}'" |
| self.result["error"] = str(e) | ||
| try: | ||
| QgsMessageLog.logMessage(str(e), "GeoAgent", level=Qgis.Warning) | ||
| except: |
There was a problem hiding this comment.
Bare except clause will catch all exceptions including system exits and keyboard interrupts, which can make the code difficult to debug and interrupt. Consider catching specific exception types or at least Exception instead of using a bare except.
| except: | |
| except Exception: |
| get_processing_logger, | ||
| set_processing_ui_log_handler, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "UILogHandler", | ||
| "get_logger", | ||
| "UILogSignal", | ||
| "get_processing_logger", | ||
| "set_processing_ui_log_handler", |
There was a problem hiding this comment.
Using inconsistent indentation - tabs instead of spaces. The rest of the codebase uses spaces for indentation (as seen in other files), but this file uses tabs on lines 6 and 7. Python PEP 8 recommends using 4 spaces per indentation level, and mixing tabs and spaces can cause issues.
| get_processing_logger, | |
| set_processing_ui_log_handler, | |
| ) | |
| __all__ = [ | |
| "UILogHandler", | |
| "get_logger", | |
| "UILogSignal", | |
| "get_processing_logger", | |
| "set_processing_ui_log_handler", | |
| get_processing_logger, | |
| set_processing_ui_log_handler, | |
| ) | |
| __all__ = [ | |
| "UILogHandler", | |
| "get_logger", | |
| "UILogSignal", | |
| "get_processing_logger", | |
| "set_processing_ui_log_handler", |
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Grammar fix looks good but there are two trailing blank lines (78-80) which is inconsistent with the project style. Most Python files should end with a single blank line.
| - new_qgis_project('/geoagent/my_project.qgs') | ||
| - new_qgis_project('/geoagent/test_project.qgz', 'QGIS Test Project') |
There was a problem hiding this comment.
The docstring examples still reference the old function name "new_qgis_project" instead of the new function name "create_new_qgis_project". This will confuse users reading the documentation.
| - new_qgis_project('/geoagent/my_project.qgs') | |
| - new_qgis_project('/geoagent/test_project.qgz', 'QGIS Test Project') | |
| - create_new_qgis_project('/geoagent/my_project.qgs') | |
| - create_new_qgis_project('/geoagent/test_project.qgz', 'QGIS Test Project') |
| self.result["error"] = str(e) | ||
| try: | ||
| QgsMessageLog.logMessage(str(e), "GeoAgent", level=Qgis.Warning) | ||
| except: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | |
| except Exception: | |
| # Ignore logging failures to avoid masking the original exception. |
| self.text_browser.verticalScrollBar().setValue( | ||
| self.text_browser.verticalScrollBar().maximum() | ||
| ) | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
|
|
||
| # Update line count | ||
| self.line_count = self.text_browser.document().lineCount() | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| # trigger UI updates | ||
| try: | ||
| project.readProject.emit() | ||
| except: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| self.result["error"] = str(e) | ||
| try: | ||
| QgsMessageLog.logMessage(str(e), "GeoAgent", level=Qgis.Warning) | ||
| except: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
No description provided.